TiDB 执行计划概览

TiDB 执行计划概览 - 图1

注意

使用 MySQL 客户端连接到 TiDB 时,为避免输出结果在终端中换行,可先执行 pager less -S 命令。执行命令后,新的 EXPLAIN 的输出结果不再换行,可按右箭头 → 键水平滚动阅读输出结果。

使用 EXPLAIN 可查看 TiDB 执行某条语句时选用的执行计划。也就是说,TiDB 在考虑上数百或数千种可能的执行计划后,最终认定该执行计划消耗的资源最少、执行的速度最快。

EXPLAIN 示例如下:

  1. CREATE TABLE t (id INT NOT NULL PRIMARY KEY auto_increment, a INT NOT NULL, pad1 VARCHAR(255), INDEX(a));
  2. INSERT INTO t VALUES (1, 1, 'aaa'),(2,2, 'bbb');
  3. EXPLAIN SELECT * FROM t WHERE a = 1;

返回的结果如下:

  1. Query OK, 0 rows affected (0.96 sec)
  2. Query OK, 2 rows affected (0.02 sec)
  3. Records: 2 Duplicates: 0 Warnings: 0
  4. +-------------------------------+---------+-----------+---------------------+---------------------------------------------+
  5. | id | estRows | task | access object | operator info |
  6. +-------------------------------+---------+-----------+---------------------+---------------------------------------------+
  7. | IndexLookUp_10 | 10.00 | root | | |
  8. | ├─IndexRangeScan_8(Build) | 10.00 | cop[tikv] | table:t, index:a(a) | range:[1,1], keep order:false, stats:pseudo |
  9. | └─TableRowIDScan_9(Probe) | 10.00 | cop[tikv] | table:t | keep order:false, stats:pseudo |
  10. +-------------------------------+---------+-----------+---------------------+---------------------------------------------+
  11. 3 rows in set (0.00 sec)

EXPLAIN 实际不会执行查询。EXPLAIN ANALYZE 可用于实际执行查询并显示执行计划。如果 TiDB 所选的执行计划非最优,可用 EXPLAINEXPLAIN ANALYZE 来进行诊断。有关 EXPLAIN 用法的详细内容,参阅以下文档:

解读 EXPLAIN 的返回结果

EXPLAIN 的返回结果包含以下字段:

  • id 为算子名,或执行 SQL 语句需要执行的子任务。详见算子简介
  • estRows 为显示 TiDB 预计会处理的行数。该预估数可能基于字典信息(例如访问方法基于主键或唯一键),或基于 CMSketch 或直方图等统计信息估算而来。
  • task 显示算子在执行语句时的所在位置。详见 Task 简介
  • access-object 显示被访问的表、分区和索引。显示的索引为部分索引。以上示例中 TiDB 使用了 a 列的索引。尤其是在有组合索引的情况下,该字段显示的信息很有参考意义。
  • operator info 显示访问表、分区和索引的其他信息。详见 operator info 结果

TiDB 执行计划概览 - 图2

注意

在执行计划返回结果中,自 v6.4.0 版本起,特定算子(即 IndexJoinApply 算子的 Probe 端所有子节点)的 estRows 字段意义与 v6.4.0 版本之前的有所不同。

在 v6.4.0 之前,estRows 表示对于 Build 端子节点的每一行,Probe 端预计会处理的行数。自 v6.4.0 起,estRows 表示 Probe 端预计会处理的总行数。由于 EXPLAIN ANALYZE 中展示的实际行数(actRows 列)表示的是总行数,v6.4.0 起这些算子 estRows 的含义与 actRows 列的含义保持一致。

例如:

  1. CREATE TABLE t1(a INT, b INT);
  2. CREATE TABLE t2(a INT, b INT, INDEX ia(a));
  3. EXPLAIN SELECT /*+ INL_JOIN(t2) */ * FROM t1 JOIN t2 ON t1.a = t2.a;
  4. EXPLAIN SELECT (SELECT a FROM t2 WHERE t2.a = t1.b LIMIT 1) FROM t1;
  1. -- v6.4.0 之前:
  2. +---------------------------------+----------+-----------+-----------------------+-----------------------------------------------------------------------------------------------------------------+
  3. | id | estRows | task | access object | operator info |
  4. +---------------------------------+----------+-----------+-----------------------+-----------------------------------------------------------------------------------------------------------------+
  5. | IndexJoin_12 | 12487.50 | root | | inner join, inner:IndexLookUp_11, outer key:test.t1.a, inner key:test.t2.a, equal cond:eq(test.t1.a, test.t2.a) |
  6. | ├─TableReader_24(Build) | 9990.00 | root | | data:Selection_23 |
  7. | └─Selection_23 | 9990.00 | cop[tikv] | | not(isnull(test.t1.a)) |
  8. | └─TableFullScan_22 | 10000.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo |
  9. | └─IndexLookUp_11(Probe) | 1.25 | root | | |
  10. | ├─Selection_10(Build) | 1.25 | cop[tikv] | | not(isnull(test.t2.a)) |
  11. | └─IndexRangeScan_8 | 1.25 | cop[tikv] | table:t2, index:ia(a) | range: decided by [eq(test.t2.a, test.t1.a)], keep order:false, stats:pseudo |
  12. | └─TableRowIDScan_9(Probe) | 1.25 | cop[tikv] | table:t2 | keep order:false, stats:pseudo |
  13. +---------------------------------+----------+-----------+-----------------------+-----------------------------------------------------------------------------------------------------------------+
  14. +---------------------------------+----------+-----------+-----------------------+------------------------------------------------------------------------------+
  15. | id | estRows | task | access object | operator info |
  16. +---------------------------------+----------+-----------+-----------------------+------------------------------------------------------------------------------+
  17. | Projection_12 | 10000.00 | root | | test.t2.a |
  18. | └─Apply_14 | 10000.00 | root | | CARTESIAN left outer join |
  19. | ├─TableReader_16(Build) | 10000.00 | root | | data:TableFullScan_15 |
  20. | └─TableFullScan_15 | 10000.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo |
  21. | └─Limit_17(Probe) | 1.00 | root | | offset:0, count:1 |
  22. | └─IndexReader_21 | 1.00 | root | | index:Limit_20 |
  23. | └─Limit_20 | 1.00 | cop[tikv] | | offset:0, count:1 |
  24. | └─IndexRangeScan_19 | 1.00 | cop[tikv] | table:t2, index:ia(a) | range: decided by [eq(test.t2.a, test.t1.b)], keep order:false, stats:pseudo |
  25. +---------------------------------+----------+-----------+-----------------------+------------------------------------------------------------------------------+
  26. -- v6.4.0 起:
  27. -- 可以发现 `IndexLookUp_11``Selection_10``IndexRangeScan_8` `TableRowIDScan_9` `estRows` 列显示的行数与 v6.4.0 以前的不同
  28. +---------------------------------+----------+-----------+-----------------------+-----------------------------------------------------------------------------------------------------------------+
  29. | id | estRows | task | access object | operator info |
  30. +---------------------------------+----------+-----------+-----------------------+-----------------------------------------------------------------------------------------------------------------+
  31. | IndexJoin_12 | 12487.50 | root | | inner join, inner:IndexLookUp_11, outer key:test.t1.a, inner key:test.t2.a, equal cond:eq(test.t1.a, test.t2.a) |
  32. | ├─TableReader_24(Build) | 9990.00 | root | | data:Selection_23 |
  33. | └─Selection_23 | 9990.00 | cop[tikv] | | not(isnull(test.t1.a)) |
  34. | └─TableFullScan_22 | 10000.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo |
  35. | └─IndexLookUp_11(Probe) | 12487.50 | root | | |
  36. | ├─Selection_10(Build) | 12487.50 | cop[tikv] | | not(isnull(test.t2.a)) |
  37. | └─IndexRangeScan_8 | 12500.00 | cop[tikv] | table:t2, index:ia(a) | range: decided by [eq(test.t2.a, test.t1.a)], keep order:false, stats:pseudo |
  38. | └─TableRowIDScan_9(Probe) | 12487.50 | cop[tikv] | table:t2 | keep order:false, stats:pseudo |
  39. +---------------------------------+----------+-----------+-----------------------+-----------------------------------------------------------------------------------------------------------------+
  40. -- 可以发现 `Limit_17``IndexReader_21``Limit_20` `IndexRangeScan_19` `estRows` 列显示的行数与 v6.4.0 以前的不同
  41. +---------------------------------+----------+-----------+-----------------------+------------------------------------------------------------------------------+
  42. | id | estRows | task | access object | operator info |
  43. +---------------------------------+----------+-----------+-----------------------+------------------------------------------------------------------------------+
  44. | Projection_12 | 10000.00 | root | | test.t2.a |
  45. | └─Apply_14 | 10000.00 | root | | CARTESIAN left outer join |
  46. | ├─TableReader_16(Build) | 10000.00 | root | | data:TableFullScan_15 |
  47. | └─TableFullScan_15 | 10000.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo |
  48. | └─Limit_17(Probe) | 10000.00 | root | | offset:0, count:1 |
  49. | └─IndexReader_21 | 10000.00 | root | | index:Limit_20 |
  50. | └─Limit_20 | 10000.00 | cop[tikv] | | offset:0, count:1 |
  51. | └─IndexRangeScan_19 | 10000.00 | cop[tikv] | table:t2, index:ia(a) | range: decided by [eq(test.t2.a, test.t1.b)], keep order:false, stats:pseudo |
  52. +---------------------------------+----------+-----------+-----------------------+------------------------------------------------------------------------------+

算子简介

算子是为返回查询结果而执行的特定步骤。真正执行扫表(读盘或者读 TiKV Block Cache)操作的算子有如下几类:

  • TableFullScan:全表扫描。
  • TableRangeScan:带有范围的表数据扫描。
  • TableRowIDScan:根据上层传递下来的 RowID 扫描表数据。时常在索引读操作后检索符合条件的行。
  • IndexFullScan:另一种“全表扫描”,扫的是索引数据,不是表数据。
  • IndexRangeScan:带有范围的索引数据扫描操作。

TiDB 会汇聚 TiKV/TiFlash 上扫描的数据或者计算结果,这种“数据汇聚”算子目前有如下几类:

  • TableReader:将 TiKV 上底层扫表算子 TableFullScan 或 TableRangeScan 得到的数据进行汇总。
  • IndexReader:将 TiKV 上底层扫表算子 IndexFullScan 或 IndexRangeScan 得到的数据进行汇总。
  • IndexLookUp:先汇总 Build 端 TiKV 扫描上来的 RowID,再去 Probe 端上根据这些 RowID 精确地读取 TiKV 上的数据。Build 端是 IndexFullScanIndexRangeScan 类型的算子,Probe 端是 TableRowIDScan 类型的算子。
  • IndexMerge:和 IndexLookupReader 类似,可以看做是它的扩展,可以同时读取多个索引的数据,有多个 Build 端,一个 Probe 端。执行过程也很类似,先汇总所有 Build 端 TiKV 扫描上来的 RowID,再去 Probe 端上根据这些 RowID 精确地读取 TiKV 上的数据。Build 端是 IndexFullScanIndexRangeScan 类型的算子,Probe 端是 TableRowIDScan 类型的算子。

算子的执行顺序

算子的结构是树状的,但在查询执行过程中,并不严格要求子节点任务在父节点之前完成。TiDB 支持同一查询内的并行处理,即子节点“流入”父节点。父节点、子节点和同级节点可能并行执行查询的一部分。

在以上示例中,├─IndexRangeScan_8(Build) 算子为 a(a) 索引所匹配的行查找内部 RowID。└─TableRowIDScan_9(Probe) 算子随后从表中检索这些行。

Build 总是先于 Probe 执行,并且 Build 总是出现在 Probe 前面。即如果一个算子有多个子节点,子节点 ID 后面有 Build 关键字的算子总是先于有 Probe 关键字的算子执行。TiDB 在展现执行计划的时候,Build 端总是第一个出现,接着才是 Probe 端。

范围查询

WHERE/HAVING/ON 条件中,TiDB 优化器会分析主键或索引键的查询返回。如数字、日期类型的比较符,如大于、小于、等于以及大于等于、小于等于,字符类型的 LIKE 符号等。

若要使用索引,条件必须是 “Sargable” (Search ARGument ABLE) 的。例如条件 YEAR(date_column) < 1992 不能使用索引,但 date_column < '1992-01-01 就可以使用索引。

推荐使用同一类型的数据以及同一类型的字符串和排序规则进行比较,以避免引入额外的 cast 操作而导致不能利用索引。

可以在范围查询条件中使用 AND(求交集)和 OR(求并集)进行组合。对于多维组合索引,可以对多个列使用条件。例如对组合索引 (a, b, c)

  • a 为等值查询时,可以继续求 b 的查询范围。
  • b 也为等值查询时,可以继续求 c 的查询范围。
  • 反之,如果 a 为非等值查询,则只能求 a 的范围。

Task 简介

目前 TiDB 的计算任务分为两种不同的 task:cop task 和 root task。Cop task 是指使用 TiKV 中的 Coprocessor 执行的计算任务,root task 是指在 TiDB 中执行的计算任务。

SQL 优化的目标之一是将计算尽可能地下推到 TiKV 中执行。TiKV 中的 Coprocessor 能支持大部分 SQL 内建函数(包括聚合函数和标量函数)、SQL LIMIT 操作、索引扫描和表扫描。

operator info 结果

EXPLAIN 返回结果中 operator info 列可显示诸如条件下推等信息。本文以上示例中,operator info 结果各字段解释如下:

  • range: [1,1] 表示查询的 WHERE 字句 (a = 1) 被下推到了 TiKV,对应的 task 为 cop[tikv]
  • keep order:false 表示该查询的语义不需要 TiKV 按顺序返回结果。如果查询指定了排序(例如 SELECT * FROM t WHERE a = 1 ORDER BY id),该字段的返回结果为 keep order:true
  • stats:pseudo 表示 estRows 显示的预估数可能不准确。TiDB 定期在后台更新统计信息。也可以通过执行 ANALYZE TABLE t 来手动更新统计信息。

EXPLAIN 执行后,不同算子返回不同的信息。你可以使用 Optimizer Hints 来控制优化器的行为,以此控制物理算子的选择。例如 /*+ HASH_JOIN(t1, t2) */ 表示优化器将使用 Hash Join 算法。详细内容见 Optimizer Hints

算子相关的系统变量

TiDB 在 MySQL 的基础上,定义了一些专用的系统变量和语法用来优化性能。其中一些系统变量和具体的算子相关,比如算子的并发度,算子的内存使用上限,是否允许使用分区表等。这些都可以通过系统变量进行控制,从而影响各个算子执行的效率。

如果读者想要详细了解所有的系统变量及其使用规则,可以参见系统变量和语法

另请参阅