免费试用
作者: Yu Dong
产品技术解读
2022-04-25

「TiDB 查询优化及调优」系列文章将通过一些具体的案例,向大家介绍 TiDB 查询及优化相关的原理和应用,在上一篇文章中我们简要介绍了 TiDB 查询优化器的优化流程。

查询计划(execution plan)展现了数据库执行 SQL 语句的具体步骤,例如通过索引还是全表扫描访问表中的数据,连接查询的实现方式和连接的顺序等。查阅及理解 TiDB 的查询计划是查询调优的基础。本文为系列文章的第二篇,将着重介绍 TiDB 查询计划以及如何查看。

下载 TiDB 社区版 咨询 TiDB 企业版
免费试用 TiDB Cloud
适用于中国出海企业和开发者

算子及 Task

在上文的 TiDB 查询优化流程简介中有提到过,TiDB 的查询计划是由一系列的执行算子构成,这些算子是为返回查询结果而执行的特定步骤,例如表扫描算子,聚合算子,Join 算子等,下面以表扫描算子为例,其它算子的具体解释可以参看下文查看执行计划的小结。

执行表扫描(读盘或者读 TiKV Block Cache)操作的算子有如下几类:

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

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

SQL 优化的目标之一是将计算尽可能地下推到 TiKV 中执行。TiKV 中的 Coprocessor 能支持大部分 SQL 内建函数(包括聚合函数和标量函数)、SQLLIMIT操作、索引扫描和表扫描。但是,所有的 Join 操作都只能作为 root task 在 TiDB 上执行。

利用 EXPLAIN 查看分析查询计划

与其它主流商业数据库一样,TiDB中可以通过解释语句返回的结果查看某条SQL的执行计划。

EXPLAIN 语句

目前 TiDB 的 EXPLAIN 主要输出 5 列,分别是:idestRowstaskaccess objectoperator info。执行计划中每个算子都由这 5 列属性来描述,EXPLAIN结果中每一行描述一个算子。每个属性的具体含义如下:

1.png

EXPLAIN ANALYZE 语句

EXPLAIN不同,EXPLAIN ANALYZE会执行对应的 SQL 语句,记录其运行时信息,和执行计划一并返回出来,可以视为EXPLAIN语句的扩展。EXPLAIN ANALYZE语句的返回结果中增加了actRows,execution info,memory,disk这几列信息:

2.png

例如在下例中,优化器估算的estRows和实际执行中统计得到的actRows几乎是相等的,说明优化器估算的行数与实际行数的误差很小。同时IndexLookUp_10算子在实际执行过程中使用了约 9 KB 的内存,该 SQL 在执行过程中,没有触发过任何算子的落盘操作。

mysql> explain analyze select * from t where a < 10; +-------------------------------+---------+---------+-----------+-------------------------+------------------------------------------------------------------------+-----------------------------------------------------+---------------+------+ | id | estRows | actRows | task | access object | execution info | operator info | memory | disk | +-------------------------------+---------+---------+-----------+-------------------------+------------------------------------------------------------------------+-----------------------------------------------------+---------------+------+ | IndexLookUp_10 | 9.00 | 9 | root | | time:641.245µs, loops:2, rpc num: 1, rpc time:242.648µs, proc keys:0 | | 9.23046875 KB | N/A | | ├─IndexRangeScan_8(Build) | 9.00 | 9 | cop[tikv] | table:t, index:idx_a(a) | time:142.94µs, loops:10, | range:[-inf,10), keep order:false | N/A | N/A | | └─TableRowIDScan_9(Probe) | 9.00 | 9 | cop[tikv] | table:t | time:141.128µs, loops:10 | keep order:false | N/A | N/A | +-------------------------------+---------+---------+-----------+-------------------------+------------------------------------------------------------------------+-----------------------------------------------------+---------------+------+ 3 rows in set (0.00 sec)

查看计划中算子的执行顺序

TiDB 的执行计划是一个树形结构,树中每个节点即是算子。考虑到每个算子内多线程并发执行的情况,在一条 SQL 执行的过程中,如果能够有一个手术刀把这棵树切开看看,大家可能会发现所有的算子都正在消耗 CPU 和内存处理数据,从这个角度来看,算子是没有执行顺序的。

但是如果从一行数据先后被哪些算子处理的角度来看,一条数据在算子上的执行是有顺序的。这个顺序可以通过下面这个规则简单总结出来:

Build总是先于Probe执行,并且Build总是出现Probe前面

这个原则的前半句是说:如果一个算子有多个子节点,子节点 ID 后面有Build关键字的算子总是先于有Probe关键字的算子执行。后半句是说:TiDB 在展现执行计划的时候,Build端总是第一个出现,接着才是Probe端。例如:

TiDB (root@127.0.0。1:test) > explain select * from t use index(idx_a) where a = 1; +-------------------------------+---------+-----------+-------------------------+---------------------------------------------+ | id | estRows | task | access object | operator info | +-------------------------------+---------+-----------+-------------------------+---------------------------------------------+ | IndexLookUp_7 | 10.00 | root | | | | ├─IndexRangeScan_5(Build) | 10.00 | cop[tikv] | table:t, index:idx_a(a) | range:[1,1], keep order:false, stats:pseudo | | └─TableRowIDScan_6(Probe) | 10.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-------------------------------+---------+-----------+-------------------------+---------------------------------------------+ 3 rows in set (0.00 sec)

这里IndexLookUp_7算子有两个孩子节点:IndexRangeScan_5(Build)TableRowIDScan_6(Probe)。可以看到,IndexRangeScan_5(Build)是第一个出现的,并且基于上面这条规则,要得到一条数据,需要先执行它得到一个RowID以后,再由TableRowIDScan_6(Probe)根据前者读上来的RowID去获取完整的一行数据。

这种规则隐含的另一个信息是:在同一层级的节点中,出现在最前面的算子可能是最先被执行的,而出现在最末尾的算子可能是最后被执行的。

例如下面这个例子:

TiDB (root@127.0.0。1:test) > explain select * from t t1 use index(idx_a) join t t2 use index() where t1.a = t2.a; +----------------------------------+----------+-----------+--------------------------+------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +----------------------------------+----------+-----------+--------------------------+------------------------------------------------------------------+ | HashJoin_22 | 12487.50 | root | | inner join, inner:TableReader_26, equal:[eq(test.t.a, test.t.a)] | | ├─TableReader_26(Build) | 9990.00 | root | | data:Selection_25 | | │ └─Selection_25 | 9990.00 | cop[tikv] | | not(isnull(test.t.a)) | | │ └─TableFullScan_24 | 10000.00 | cop[tikv] | table:t2 | keep order:false, stats:pseudo | | └─IndexLookUp_29(Probe) | 9990.00 | root | | | | ├─IndexFullScan_27(Build) | 9990.00 | cop[tikv] | table:t1, index:idx_a(a) | keep order:false, stats:pseudo | | └─TableRowIDScan_28(Probe) | 9990.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | +----------------------------------+----------+-----------+--------------------------+------------------------------------------------------------------+ 7 rows in set (0.00 sec)

要完成HashJoin_22,需要先执行TableReader_26(Build)再执行IndexLookUp_29(Probe)。而在执行IndexLookUp_29(Probe)的时候,又需要先执行IndexFullScan_27(Build)再执行TableRowIDScan_28(Probe)。所以从整条执行链路来看,TableRowIDScan_28(Probe)是最后被唤起执行的。

查看表扫描的执行计划

在上文介绍算子和任务时已经提到过表扫描算子,这里再稍微重复介绍一下,分为执行表扫描操作的算子和对扫描数据进行汇聚和计算的算子:

执行表扫描(读盘或者读 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类型的算子。

IndexLookUp 示例:

mysql> explain select * from t use index(idx_a); +-------------------------------+----------+-----------+-------------------------+--------------------------------+ | id | estRows | task | access object | operator info | +-------------------------------+----------+-----------+-------------------------+--------------------------------+ | IndexLookUp_6 | 10000.00 | root | | | | ├─IndexFullScan_4(Build) | 10000.00 | cop[tikv] | table:t, index:idx_a(a) | keep order:false, stats:pseudo | | └─TableRowIDScan_5(Probe) | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-------------------------------+----------+-----------+-------------------------+--------------------------------+ 3 rows in set (0.00 sec)

这里IndexLookUp_6算子有两个孩子节点:IndexFullScan_4(Build)TableRowIDScan_5(Probe)。可以看到,IndexFullScan_4(Build)执行索引全表扫,扫描索引a的所有数据,因为是全范围扫,这个操作将获得表中所有数据的RowID,之后再由TableRowIDScan_5(Probe)去根据这些RowID去扫描所有的表数据。可以预见的是,这个执行计划不如直接使用 TableReader 进行全表扫,因为同样都是全表扫,这里的IndexLookUp多扫了一次索引,带来了额外的开销。

TableReader 示例:

mysql> explain select * from t where a > 1 or b >100; +-------------------------+----------+-----------+---------------+----------------------------------------+ | id | estRows | task | access object | operator info | +-------------------------+----------+-----------+---------------+----------------------------------------+ | TableReader_7 | 8000.00 | root | | data:Selection_6 | | └─Selection_6 | 8000.00 | cop[tikv] | | or(gt(test.t.a, 1), gt(test.t.b, 100)) | | └─TableFullScan_5 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-------------------------+----------+-----------+---------------+----------------------------------------+ 3 rows in set (0.00 sec)

在上面例子中TableReader_7算子的孩子节点是 Selection_6。以这个孩子节点为根的子树被当做了一个Cop Task下发给了相应的 TiKV,这个Cop Task使用TableFullScan_5算子执行扫表操作。Selection 表示 SQL 语句中的选择条件,可能来自 SQL 语句中的WHERE/HAVING/ON子句。由TableFullScan_5可以看到,这个执行计划使用了一个全表扫描的操作,集群的负载将因此而上升,可能会影响到集群中正在运行的其他查询。这时候如果能够建立合适的索引,并且使用IndexMerge算子,将能够极大的提升查询的性能,降低集群的负载。

IndexMerge 示例:

注意:目前 TIDB 的Index Merge为实验特性在 5.3 及以前版本中默认关闭,同时 5.0 中的Index Merge目前支持的场景仅限于析取范式(or 连接的表达式),对合取范式(and 连接的表达式)将在之后的版本中支持。 开启Index Merge特性,可通过在客户端中设置 session 或者 global 变量完成:set @@tidb_enable_index_merge = 1;

mysql> set @@tidb_enable_index_merge = 1; mysql> explain select * from t use index(idx_a, idx_b) where a > 1 or b > 1; +------------------------------+---------+-----------+-------------------------+------------------------------------------------+ | id | estRows | task | access object | operator info | +------------------------------+---------+-----------+-------------------------+------------------------------------------------+ | IndexMerge_16 | 6666.67 | root | | | | ├─IndexRangeScan_13(Build) | 3333.33 | cop[tikv] | table:t, index:idx_a(a) | range:(1,+inf], keep order:false, stats:pseudo | | ├─IndexRangeScan_14(Build) | 3333.33 | cop[tikv] | table:t, index:idx_b(b) | range:(1,+inf], keep order:false, stats:pseudo | | └─TableRowIDScan_15(Probe) | 6666.67 | cop[tikv] | table:t | keep order:false, stats:pseudo | +------------------------------+---------+-----------+-------------------------+------------------------------------------------+ 4 rows in set (0.00 sec)

IndexMerge使得数据库在扫描表数据时可以使用多个索引。这里IndexMerge_16算子有三个孩子节点,其中IndexRangeScan_13IndexRangeScan_14根据范围扫描得到符合条件的所有RowID,再由TableRowIDScan_15算子根据这些RowID精确的读取所有满足条件的数据。

查看聚合计算的执行计划

Hash Aggregate 示例:

TiDB 上的 Hash Aggregation 算子采用多线程并发优化,执行速度快,但会消耗较多内存。下面是一个 Hash Aggregate 的例子:

TiDB (root@127.0.0.1:测试)>解释选择/ * +H_AGG() */ count(*) from t; +---------------------------+----------+-----------+---------------+---------------------------------+ | id | estRows | task | access object | operator info | +---------------------------+----------+-----------+---------------+---------------------------------+ | HashAgg_11 | 1.00 | root | | funcs:count(Column#7)->Column#4 | | └─TableReader_12 | 1.00 | root | | data:HashAgg_5 | | └─HashAgg_5 | 1.00 | cop[tikv] | | funcs:count(1)->Column#7 | | └─TableFullScan_8 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +---------------------------+----------+-----------+---------------+---------------------------------+ 4 rows in set (0.00 sec)

一般而言 TiDB 的Hash Aggregate会分成两个阶段执行,一个在 TiKV/TiFlash 的Coprocessor上,计算聚合函数的中间结果。另一个在 TiDB 层,汇总所有Coprocessor Task的中间结果后,得到最终结果。

Stream Aggregate 示例:

TiDBStream Aggregation算子通常会比Hash Aggregate占用更少的内存,有些场景中也会比Hash Aggregate执行的更快。当数据量太大或者系统内存不足时,可以试试Stream Aggregate算子。一个Stream Aggregate的例子如下:

TiDB (root@127.0.0。1:test) > explain select /*+ STREAM_AGG() */ count(*) from t; +----------------------------+----------+-----------+---------------+---------------------------------+ | id | estRows | task | access object | operator info | +----------------------------+----------+-----------+---------------+---------------------------------+ | StreamAgg_16 | 1.00 | root | | funcs:count(Column#7)->Column#4 | | └─TableReader_17 | 1.00 | root | | data:StreamAgg_8 | | └─StreamAgg_8 | 1.00 | cop[tikv] | | funcs:count(1)->Column#7 | | └─TableFullScan_13 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +----------------------------+----------+-----------+---------------+---------------------------------+ 4 rows in set (0.00 sec)

Hash Aggregate类似,一般而言 TiDB 的Stream Aggregate也会分成两个阶段执行,一个在TiKV / TiFlash 的Coprocessor上,计算聚合函数的中间结果。另一个在 TiDB 层,汇总所有Coprocessor Task的中间结果后,得到最终结果。

查看 Join 的执行计划

TiDB 的 Join 算法包括如下几类:

  • Hash Join
  • Merge Join
  • Index Hash Join
  • Index Merge Join

Apply

下面分别通过一些例子来解释这些 Join 算法的执行过程

Hash Join 示例:

TiDB 的 Hash Join 算子采用了多线程优化,执行速度较快,但会消耗较多内存。一个 Hash Join 的例子如下:

mysql> explain select /*+ HASH_JOIN(t1, t2) */ * from t t1 join t2 on t1.a = t2.a; +------------------------------+----------+-----------+---------------+-------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +------------------------------+----------+-----------+---------------+-------------------------------------------------------------------+ | HashJoin_33 | 10000.00 | root | | inner join, inner:TableReader_43, equal:[eq(test.t.a, test.t2.a)] | | ├─TableReader_43(Build) | 10000.00 | root | | data:Selection_42 | | │ └─Selection_42 | 10000.00 | cop[tikv] | | not(isnull(test.t2.a)) | | │ └─TableFullScan_41 | 10000.00 | cop[tikv] | table:t2 | keep order:false | | └─TableReader_37(Probe) | 10000.00 | root | | data:Selection_36 | | └─Selection_36 | 10000.00 | cop[tikv] | | not(isnull(test.t.a)) | | └─TableFullScan_35 | 10000.00 | cop[tikv] | table:t1 | keep order:false | +------------------------------+----------+-----------+---------------+-------------------------------------------------------------------+ 7 rows in set (0.00 sec)

Hash Join会将Build端的数据缓存在内存中,根据这些数据构造出一个Hash Table,然后读取Probe端的数据,用Probe端的数据去探测(Probe)Build端构造出来的Hash Table,将符合条件的数据返回给用户。

Merge Join示例:
TiDB 的Merge Join算子相比于Hash Join通常会占用更少的内存,但可能执行时间会更久。当数据量太大,或系统内存不足时,建议尝试使用。下面是一个Merge Join的例子:

mysql >解释选择/ * + SM_JOIN (t1) * / *从tt1 join t t2 on t1.a = t2.a; +------------------------------------+----------+-----------+--------------------------+---------------------------------------------------+ | id | estRows | task | access object | operator info | +------------------------------------+----------+-----------+--------------------------+---------------------------------------------------+ | MergeJoin_6 | 10000.00 | root | | inner join, left key:test.t.a, right key:test.t.a | | ├─IndexLookUp_13(Build) | 10000.00 | root | | | | │ ├─IndexFullScan_11(Build) | 10000.00 | cop[tikv] | table:t2, index:idx_a(a) | keep order:true | | │ └─TableRowIDScan_12(Probe) | 10000.00 | cop[tikv] | table:t2 | keep order:false | | └─IndexLookUp_10(Probe) | 10000.00 | root | | | | ├─IndexFullScan_8(Build) | 10000.00 | cop[tikv] | table:t1, index:idx_a(a) | keep order:true | | └─TableRowIDScan_9(Probe) | 10000.00 | cop[tikv] | table:t1 | keep order:false | +------------------------------------+----------+-----------+--------------------------+---------------------------------------------------+ 7 rows in set (0.00 sec)

Merge Join算子在执行时,会从Build端把一个Join Group的数据全部读取到内存中,接着再去读Probe端的数据,用Probe端的每行数据去和Build端的完整的一个Join Group依次去看是否匹配(除了满足等值条件以外,还有其他非等值条件,这里的 “匹配” 主要是指查看是否满足非等值职条件)。Join Group指的是所有Join Key上值相同的数据。

Index Hash Join 示例:

INL_HASH_JOIN(t1_name [, tl_name])提示优化器使用Index Nested Loop Hash Join算法。该算法与Index Nested Loop Join使用条件完全一样,但在某些场景下会更为节省内存资源。

mysql> explain select /*+ INL_HASH_JOIN(t1) */ * from t t1 join t t2 on t1.a = t2.a; +----------------------------------+----------+-----------+--------------------------+--------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +----------------------------------+----------+-----------+--------------------------+--------------------------------------------------------------------------+ | IndexHashJoin_32 | 10000.00 | root | | inner join, inner:IndexLookUp_23, outer key:test.t.a, inner key:test.t.a | | ├─TableReader_35(Build) | 10000.00 | root | | data:Selection_34 | | │ └─Selection_34 | 10000.00 | cop[tikv] | | not(isnull(test.t.a)) | | │ └─TableFullScan_33 | 10000.00 | cop[tikv] | table:t2 | keep order:false | | └─IndexLookUp_23(Probe) | 1.00 | root | | | | ├─Selection_22(Build) | 1.00 | cop[tikv] | | not(isnull(test.t.a)) | | │ └─IndexRangeScan_20 | 1.00 | cop[tikv] | table:t1, index:idx_a(a) | range: decided by [eq(test.t.a, test.t.a)], keep order:false | | └─TableRowIDScan_21(Probe) | 1.00 | cop[tikv] | table:t1 | keep order:false | +----------------------------------+----------+-----------+--------------------------+--------------------------------------------------------------------------+ 8 rows in set (0.00 sec)

Index Merge Join 示例:
INL_MERGE_JOIN(t1_name [, tl_name])提示优化器使用Index Nested Loop Merge Join算法。该算法相比于INL_JOIN会更节省内存。该算法使用条件包含INL_JOIN的所有使用条件,但还需要添加一条:join keys中的内表列集合是内表使用的index的前缀,或内表使用的indexjoin keys中的内表列集合的前缀。

mysql> explain select /*+ INL_MERGE_JOIN(t2@sel_2) */ * from t t1 where t1.a in ( select t2.a from t t2 where t2.b < t1.b); +---------------------------------+---------+-----------+--------------------------+-----------------------------------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +---------------------------------+---------+-----------+--------------------------+-----------------------------------------------------------------------------------------------------------+ | IndexMergeJoin_23 | 6.39 | root | | semi join, inner:Projection_21, outer key:test.t.a, inner key:test.t.a, other cond:lt(test.t.b, test.t.b) | | ├─TableReader_28(Build) | 7.98 | root | | data:Selection_27 | | │ └─Selection_27 | 7.98 | cop[tikv] | | not(isnull(test.t.a)), not(isnull(test.t.b)) | | │ └─TableFullScan_26 | 8.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | | └─Projection_21(Probe) | 1.25 | root | | test.t.a, test.t.b | | └─IndexLookUp_20 | 1.25 | root | | | | ├─Selection_18(Build) | 1.25 | cop[tikv] | | not(isnull(test.t.a)) | | │ └─IndexRangeScan_16 | 1.25 | cop[tikv] | table:t2, index:idx_a(a) | range: decided by [eq(test.t.a, test.t.a)], keep order:true, stats:pseudo | | └─Selection_19(Probe) | 1.25 | cop[tikv] | | not(isnull(test.t.b)) | | └─TableRowIDScan_17 | 1.25 | cop[tikv] | table:t2 | keep order:false, stats:pseudo | +---------------------------------+---------+-----------+--------------------------+-----------------------------------------------------------------------------------------------------------+ 10 rows in set (0.01 sec) ```sql Apply 示例: ```sql mysql> explain select * from t t1 where t1.a in ( select avg(t2.a) from t2 where t2.b < t1.b); +----------------------------------+----------+-----------+---------------+-------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +----------------------------------+----------+-----------+---------------+-------------------------------------------------------------------------------+ | Projection_10 | 10000.00 | root | | test.t.id, test.t.a, test.t.b | | └─Apply_12 | 10000.00 | root | | semi join, inner:StreamAgg_30, equal:[eq(Column#8, Column#7)] | | ├─Projection_13(Build) | 10000.00 | root | | test.t.id, test.t.a, test.t.b, cast(test.t.a, decimal(20,0) BINARY)->Column#8 | | │ └─TableReader_15 | 10000.00 | root | | data:TableFullScan_14 | | │ └─TableFullScan_14 | 10000.00 | cop[tikv] | table:t1 | keep order:false | | └─StreamAgg_30(Probe) | 1.00 | root | | funcs:avg(Column#12, Column#13)->Column#7 | | └─TableReader_31 | 1.00 | root | | data:StreamAgg_19 | | └─StreamAgg_19 | 1.00 | cop[tikv] | | funcs:count(test.t2.a)->Column#12, funcs:sum(test.t2.a)->Column#13 | | └─Selection_29 | 8000.00 | cop[tikv] | | lt(test.t2.b, test.t.b) | | └─TableFullScan_28 | 10000.00 | cop[tikv] | table:t2 | keep order:false | +----------------------------------+----------+-----------+-----------------------------------------------------------------------------------------------+ 10 rows in set, 1 warning (0.00 sec)

其它关于 EXPLAIN 的说明

EXPLAIN FOR CONNECTION用于获得一个连接中最后执行的查询的执行计划,其输出格式与EXPLAIN完全一致。但 TiDB 中的实现与 MySQL 不同,除了输出格式之外,还有以下区别:

MySQL 返回的是正在执行的查询计划,而 TiDB 返回的是最后执行的查询计划。

MySQL 的文档中指出,MySQL 要求登录用户与被查询的连接相同,或者拥有PROCESS权限,而TiDB则要求登录用户与被查询的连接相同,或者拥有SUPER权限。

本文为「TiDB 查询优化及调优」系列文章的第二篇,后续将继续对 TiDB 慢查询诊断监控及排查、调整及优化查询执行计划以及其他优化器开发或规划中的诊断调优功能等进行介绍。如果您对 TiDB 的产品有任何建议,欢迎来到https://internals.tidb.io与我们交流。