From 555d414a9f6bed193357bd31de35a942e6efe65e Mon Sep 17 00:00:00 2001 From: pingcap-github-bot Date: Thu, 23 Apr 2020 11:34:22 +0800 Subject: [PATCH] sql: update `explain` results (#2548) (#2865) --- reference/performance/index-merge.md | 46 +-- .../understanding-the-query-execution-plan.md | 391 ++++++++++++++---- reference/sql/statements/add-index.md | 26 +- reference/sql/statements/alter-table.md | 26 +- reference/sql/statements/analyze-table.md | 24 +- reference/sql/statements/create-index.md | 26 +- reference/sql/statements/drop-index.md | 26 +- reference/sql/statements/explain-analyze.md | 33 +- reference/sql/statements/explain.md | 137 +++--- 9 files changed, 499 insertions(+), 236 deletions(-) diff --git a/reference/performance/index-merge.md b/reference/performance/index-merge.md index 1d7c067a3564..3a8c70edd1e9 100644 --- a/reference/performance/index-merge.md +++ b/reference/performance/index-merge.md @@ -29,13 +29,13 @@ explain select * from t where a = 1 or b = 1; 由于查询的过滤条件是一个通过 `OR` 连接的表达式,我们在只能对每张表使用一个索引的限制下,无法将 `a = 1` 下推到索引 `a` 上,或将 `b = 1` 下推到索引 `b` 上,因此为了保证结果正确性,对这个查询只能生成 `TableScan` 的执行计划: ``` -+---------------------+----------+-----------+------------------------------------------------------------+ -| id | count | task | operator info | -+---------------------+----------+-----------+------------------------------------------------------------+ -| TableReader_7 | 8000.00 | root | data:Selection_6 | -| └─Selection_6 | 8000.00 | cop[tikv] | or(eq(test.t.a, 1), eq(test.t.b, 1)) | -| └─TableScan_5 | 10000.00 | cop[tikv] | table:t, range:[-inf,+inf], keep order:false, stats:pseudo | -+---------------------+----------+-----------+------------------------------------------------------------+ ++-------------------------+----------+-----------+---------------+--------------------------------------+ +| id | estRows | task | access object | operator info | ++-------------------------+----------+-----------+---------------+--------------------------------------+ +| TableReader_7 | 8000.00 | root | | data:Selection_6 | +| └─Selection_6 | 8000.00 | cop[tikv] | | or(eq(test.t.a, 1), eq(test.t.b, 1)) | +| └─TableFullScan_5 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | ++-------------------------+----------+-----------+---------------+--------------------------------------+ ``` 当 `t` 的数据量很大时,全表扫描的效率会很低,但这条查询最多却只会返回两行记录。针对这类场景,TiDB 引入了对表的新访问方式 `IndexMerge`。 @@ -45,14 +45,14 @@ explain select * from t where a = 1 or b = 1; 在 `IndexMerge` 访问方式下,优化器可以选择对一张表使用多个索引,并将每个索引的返回结果进行集合并操作。以上面查询为例,生成的执行计划将会变为: ``` -+--------------------+-------+-----------+---------------------------------------------------------------+ -| id | count | task | operator info | -+--------------------+-------+-----------+---------------------------------------------------------------+ -| IndexMerge_11 | 2.00 | root | | -| ├─IndexScan_8 | 1.00 | cop[tikv] | table:t, index:a, range:[1,1], keep order:false, stats:pseudo | -| ├─IndexScan_9 | 1.00 | cop[tikv] | table:t, index:b, range:[1,1], keep order:false, stats:pseudo | -| └─TableScan_10 | 2.00 | cop[tikv] | table:t, keep order:false, stats:pseudo | -+--------------------+-------+-----------+---------------------------------------------------------------+ ++--------------------------------+---------+-----------+---------------------+---------------------------------------------+ +| id | estRows | task | access object | operator info | ++--------------------------------+---------+-----------+---------------------+---------------------------------------------+ +| IndexMerge_11 | 2.00 | root | | | +| ├─IndexRangeScan_8(Build) | 1.00 | cop[tikv] | table:t, index:a(a) | range:[1,1], keep order:false, stats:pseudo | +| ├─IndexRangeScan_9(Build) | 1.00 | cop[tikv] | table:t, index:b(b) | range:[1,1], keep order:false, stats:pseudo | +| └─TableRowIDScan_10(Probe) | 2.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | ++--------------------------------+---------+-----------+---------------------+---------------------------------------------+ ``` `IndexMerge` 执行计划的结构和 `IndexLookUp` 很接近,都可以分为索引扫描和全表扫描两部分,只是 `IndexMerge` 的索引扫描部分可以包含多个 `IndexScan`,当表的主键索引是整数类型时,索引扫描部分甚至可能包含一个 `TableScan`,比如: @@ -74,14 +74,14 @@ explain select * from t where a = 1 or b = 1; ``` ``` -+--------------------+-------+-----------+---------------------------------------------------------------+ -| id | count | task | operator info | -+--------------------+-------+-----------+---------------------------------------------------------------+ -| IndexMerge_11 | 2.00 | root | | -| ├─TableScan_8 | 1.00 | cop[tikv] | table:t, range:[1,1], keep order:false, stats:pseudo | -| ├─IndexScan_9 | 1.00 | cop[tikv] | table:t, index:b, range:[1,1], keep order:false, stats:pseudo | -| └─TableScan_10 | 2.00 | cop[tikv] | table:t, keep order:false, stats:pseudo | -+--------------------+-------+-----------+---------------------------------------------------------------+ ++--------------------------------+---------+-----------+---------------------+---------------------------------------------+ +| id | estRows | task | access object | operator info | ++--------------------------------+---------+-----------+---------------------+---------------------------------------------+ +| IndexMerge_11 | 2.00 | root | | | +| ├─TableRangeScan_8(Build) | 1.00 | cop[tikv] | table:t | range:[1,1], keep order:false, stats:pseudo | +| ├─IndexRangeScan_9(Build) | 1.00 | cop[tikv] | table:t, index:b(b) | range:[1,1], keep order:false, stats:pseudo | +| └─TableRowIDScan_10(Probe) | 2.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | ++--------------------------------+---------+-----------+---------------------+---------------------------------------------+ 4 rows in set (0.01 sec) ``` diff --git a/reference/performance/understanding-the-query-execution-plan.md b/reference/performance/understanding-the-query-execution-plan.md index 49c9963f0d29..7b92a67341bb 100644 --- a/reference/performance/understanding-the-query-execution-plan.md +++ b/reference/performance/understanding-the-query-execution-plan.md @@ -7,151 +7,372 @@ category: reference TiDB 优化器会根据当前数据表的实际情况来选择最优的执行计划,执行计划由一系列的算子构成。本文将详细解释 TiDB 中 `EXPLAIN` 语句返回的执行计划信息。 -## 使用 `EXPLAIN` 来优化 SQL 语句 +## EXPLAIN 简介 `EXPLAIN` 语句的返回结果提供了 TiDB 执行 SQL 查询的详细信息: -- `EXPLAIN` 可以和 `SELECT`,`DELETE` 语句一起使用; -- 执行 `EXPLAIN`,TiDB 会返回被 `EXPLAIN` 的 SQL 语句经过优化器后的最终物理执行计划。也就是说,`EXPLAIN` 展示了 TiDB 执行该 SQL 语句的完整信息,比如以什么样的顺序,什么方式 JOIN 两个表,表达式树长什么样等等。详见 [`EXPLAIN` 输出格式](#explain-输出格式); -- TiDB 支持 `EXPLAIN [options] FOR CONNECTION connection_id`,但与 MySQL 的 `EXPLAIN FOR` 有一些区别,请参见 [`EXPLAIN FOR CONNECTION`](#explain-for-connection)。 +- `EXPLAIN` 可以和 `SELECT`,`DELETE` 等语句一起使用; +- 执行 `EXPLAIN`,TiDB 会返回被 `EXPLAIN` 的 SQL 语句经过优化器后的最终物理执行计划。也就是说,`EXPLAIN` 展示了 TiDB 执行该 SQL 语句的完整信息,比如以什么样的顺序,什么方式 JOIN 两个表,表达式树长什么样等等。 +- 关于 `EXPLAIN` 每列的简述,可以参见 [EXPLAIN 输出格式](/reference/sql/statements/explain.md)。 通过观察 `EXPLAIN` 的结果,你可以知道如何给数据表添加索引使得执行计划使用索引从而加速 SQL 语句的执行速度;你也可以使用 `EXPLAIN` 来检查优化器是否选择了最优的顺序来 JOIN 数据表。 -## `EXPLAIN` 输出格式 +### 算子的执行顺序 -目前 TiDB 的 `EXPLAIN` 会输出 4 列,分别是:id,count,task,operator info。执行计划中每个算子都由这 4 列属性来描述,`EXPLAIN` 结果中每一行描述一个算子。每个属性的具体含义如下: +TiDB 的执行计划是一个树形结构,树中每个节点即是算子。考虑到每个算子内多线程并发执行的情况,在一条 SQL 执行的过程中,如果能够有一个手术刀把这棵树切开看看,大家可能会发现所有的算子都正在消耗 CPU 和内存处理数据,从这个角度来看,算子是没有执行顺序的。 -| 属性名 | 含义 | -|:----------------|:--------------------------------------------------------------------------------------------------------------------------------------------| -| id | 算子的 ID,在整个执行计划中唯一的标识一个算子。在 TiDB 2.1 中,id 会格式化显示算子的树状结构。数据从 child 流向 parent,每个 算子的 parent 有且仅有一个。 | -| count | 预计当前算子将会输出的数据条数,基于统计信息以及算子的执行逻辑估算而来。 | -| task | 当前这个算子属于什么 task。目前的执行计划分成为两种 task,一种叫 **root** task,在 tidb-server 上执行,一种叫 **cop** task,并行的在 TiKV 上执行。当前的执行计划在 task 级别的拓扑关系是一个 root task 后面可以跟许多 cop task,root task 使用 cop task 的输出结果作为输入。cop task 中执行的也即是 TiDB 下推到 TiKV 上的任务,每个 cop task 分散在 TiKV 集群中,由多个进程共同执行。 | -| operator info | 每个算子的详细信息。各个算子的 operator info 各有不同,详见 [Operator Info](#operator-info)。 | +但是如果从一行数据先后被哪些算子处理的角度来看,一条数据在算子上的执行是有顺序的。这个顺序可以通过下面这个规则简单总结出来: -## `EXPLAIN ANALYZE` 输出格式 +**Build 总是先于 Probe 执行,并且 Build 总是出现在 Probe 前面。** -作为 `EXPLAIN` 语句的扩展,`EXPLAIN ANALYZE` 语句执行查询并在 `execution info` 列中提供额外的执行统计信息。具体如下: +这个原则的前半句是说:如果一个算子有多个孩子节点,孩子节点 ID 后面有 Build 关键字的算子总是先于有 Probe 关键字的算子执行。后半句是说:TiDB 在展现执行计划的时候,Build 端总是第一个出现,接着才是 Probe 端。 -* `time` 显示从进入算子到离开算子的全部 wall time,包括所有子算子操作的全部执行时间。如果该算子被父算子多次调用 (`loops`),这个时间就是累积的时间。 -* `loops` 是当前算子被父算子调用的次数。 -* `rows` 是当前算子返回的行的总数。例如,可以将 `count` 列的精度和 `execution_info` 列中的 `rows`/`loops` 值进行对比,据此评定查询优化器估算的精确度。 -* `memory` 是当前算子使用的内存信息,只有 root task 才会有此类信息,如果没有此信息则显示 `N/A`。 -* `disk` 是当前算子使用的磁盘信息,只有 root task 才会有此类信息,如果没有使用磁盘,则会显示 `N/A`。 +一些例子: -### 用例 +``` +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) +``` -使用 [bikeshare example database](https://github.com/pingcap/docs/blob/master/dev/how-to/get-started/import-example-database.md): +这里 `IndexLookUp_7` 算子有两个孩子节点:`IndexRangeScan_5(Build)` 和 `TableRowIDScan_6(Probe)`。可以看到,`IndexRangeScan_5(Build)` 是第一个出现的,并且基于上面这条规则,要得到一条数据,需要先执行 `IndexRangeScan_5(Build)` 得到一个 RowID 以后,再由 `TableRowIDScan_6(Probe)` 根据前者读上来的 RowID 去获取完整的一行数据。 -{{< copyable "sql" >}} +这种规则隐含的另一个信息是:在同一层级的节点中,出现在最前面的算子可能是最先被执行的,而出现在最末尾的算子可能是最后被执行的。比如下面这个例子: -```sql -EXPLAIN SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59'; ``` +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)` 是最后被唤起执行的。 + +### Task 简介 + +目前 TiDB 的计算任务隶属于两种不同的 task:cop task 和 root task。cop task 是指使用 TiKV 中的 coprocessor 执行的计算任务,root task 是指在 TiDB 中执行的计算任务。 + +SQL 优化的目标之一是将计算尽可能地下推到 TiKV 中执行。TiKV 中的 coprocessor 能支持大部分 SQL 内建函数(包括聚合函数和标量函数)、SQL `LIMIT` 操作、索引扫描和表扫描。但是,所有的 Join 操作都只能作为 root task 在 TiDB 上执行。 + +### 范围查询 + +在 WHERE/HAVING/ON 条件中,TiDB 优化器会分析主键或索引键的查询返回。如数字、日期类型的比较符,如大于、小于、等于以及大于等于、小于等于,字符类型的 LIKE 符号等。 + +值得注意的是,TiDB 目前只支持比较符一端是列,另一端是常量,或可以计算成某一常量的情况,类似 `year(birth_day) < 1992` 的查询条件是不能利用索引的。还要注意应尽可能使用同一类型进行比较,以避免引入额外的 cast 操作而导致不能利用索引,如 `user_id = 123456`,如果 user_id 是字符串,需要将 123456 也写成字符串常量的形式。 + +针对同一列的范围查询条件使用 AND 和 OR 组合后,等于对范围求交集或者并集。对于多维组合索引,可以写多个列的条件。例如对组合索引 (a, b, c),当 a 为等值查询时,可以继续求 b 的查询范围,当 b 也为等值查询时,可以继续求 c 的查询范围;反之,如果 a 为非等值查询,则只能求 a 的范围。 + +## 算子信息 + +不同的算子在 explain 时输出的信息各有不同,接下来这部分将会讲述如何阅读各种扫表、聚合、连接算子的执行计划。 + +可以使用优化器提示来控制优化器的行为,以此控制物理算子的选择,如使用 `/*+ HASH_JOIN(t1, t2) */` 来提示优化器使用 Hash Join 算法,详见[优化器提示](/reference/performance/optimizer-hints.md)。 + +如果读者想要详细了解各算子的内部实现,可以参见[源码阅读系列](https://pingcap.com/blog-cn/#TiDB-源码阅读)。 + +### 如何阅读扫表的执行计划 + +真正执行扫表(读盘或者读 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 端是 IndexFullScan 或 IndexRangeScan 类型的算子,Probe 端是 TableRowIDScan 类型的算子。 +- **IndexMerge**:和 IndexLookupReader 类似,可以看做是它的扩展,可以同时读取多个索引的数据,有多个 Build 端,一个 Probe 端。执行过程也很类似,先汇总所有 Build 端 TiKV 扫描上来的 RowID,再去 Probe 端上根据这些 RowID 精确地读取 TiKV 上的数据。Build 端是 IndexFullScan 或 IndexRangeScan 类型的算子,Probe 端是 TableRowIDScan 类型的算子。 + +#### 表数据和索引数据 + +TiDB 的表数据是指一张表的原始数据,存放在 TiKV 中。对于每行表数据,它的 key 是一个 64 位整数,称为 RowID。如果一张表存在 int 类型的主键,TiDB 会把主键的值当作表数据的 RowID,否则由系统自动生成 RowID。表数据的 value 由这一行的所有数据编码而成。在读取表数据的时候,可以按照 RowID 递增的顺序返回。 + +TiDB 的索引数据和表数据一样,也存放在 TiKV 中。它的 key 是由索引列编码的有序 bytes,value 是这一行索引数据对应的 RowID,通过 RowID 可以读取这一行的非索引列。在读取索引数据的时候,TiKV 会按照索引列递增的顺序返回,如果有多个索引列,首先保证第 1 列递增,并且在第 i 列相等的情况下,保证第 i + 1 列递增。 + +#### IndexLookUp 示例 ``` -+--------------------------+-------------+------+------------------------------------------------------------------------------------------------------------------------+ -| id | count | task | operator info | -+--------------------------+-------------+------+------------------------------------------------------------------------------------------------------------------------+ -| StreamAgg_20 | 1.00 | root | funcs:count(col_0) | -| └─TableReader_21 | 1.00 | root | data:StreamAgg_9 | -| └─StreamAgg_9 | 1.00 | cop | funcs:count(1) | -| └─Selection_19 | 8166.73 | cop | ge(bikeshare.trips.start_date, 2017-07-01 00:00:00.000000), le(bikeshare.trips.start_date, 2017-07-01 23:59:59.000000) | -| └─TableScan_18 | 19117643.00 | cop | table:trips, range:[-inf,+inf], keep order:false | -+--------------------------+-------------+------+------------------------------------------------------------------------------------------------------------------------+ -5 rows in set (0.00 sec) +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) ``` -在上面的例子中,coprocessor 上读取 `trips` 表上的数据(`TableScan_18`),寻找满足 `start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59'` 条件的数据(`Selection_19`),然后计算满足条件的数据行数(`StreamAgg_9`),最后把结果返回给 TiDB。TiDB 汇总各个 coprocessor 返回的结果(`TableReader_21`),并进一步计算所有数据的行数(`StreamAgg_20`),最终把结果返回给客户端。在上面这个查询中,TiDB 根据 `trips` 表的统计信息估算出 `TableScan_18` 的输出结果行数为 19117643.00,满足条件 `start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59'` 的有 `8166.73` 条,经过聚合运算后,只有 1 条结果。 +这里 `IndexLookUp_6` 算子有两个孩子节点:`IndexFullScan_4(Build)` 和 `TableRowIDScan_5(Probe)`。可以看到,`IndexFullScan_4(Build)` 执行索引全表扫,扫描索引 a 的所有数据,因为是全范围扫,这个操作将获得表中所有数据的 RowID,之后再由 `TableRowIDScan_5(Probe)` 根据这些 RowID 去扫描所有的表数据。可以预见的是,这个执行计划不如直接使用 TableReader 进行全表扫,因为同样都是全表扫,这里的 IndexLookUp 多扫了一次索引,带来了额外的开销。 -上述查询中,虽然大部分计算逻辑都下推到了 TiKV 的 coprocessor 上,但是其执行效率还是不够高,可以添加适当的索引来消除 `TableScan_18` 对 `trips` 的全表扫,进一步加速查询的执行: +#### TableReader 示例 -{{< copyable "sql" >}} +``` +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 示例 -```sql -ALTER TABLE trips ADD INDEX (start_date); +``` +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) ``` -{{< copyable "sql" >}} +IndexMerge 使得数据库在扫描表数据时可以使用多个索引。这里 `IndexMerge_16` 算子有三个孩子节点,其中 `IndexRangeScan_13` 和 `IndexRangeScan_14` 根据范围扫描得到符合条件的所有 RowID,再由 `TableRowIDScan_15` 算子根据这些 RowID 精确地读取所有满足条件的数据。 -```sql -EXPLAIN SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59'; +> **注意:** +> +> 目前 TiDB 的 IndexMerge 特性在 4.0 RC 版本中默认关闭,同时 4.0 中的 IndexMerge 目前支持的场景仅限于析取范式(or 连接的表达式),对合取范式(and 连接的表达式)将在之后的版本中支持。 +> 开启 IndexMerge 特性,可通过在客户端中设置 session 或者 global 变量完成:`set @@tidb_enable_index_merge = 1;` + +### 如何阅读聚合的执行计划 + +TiDB 的聚合算法包括如下两类: + +- Hash Aggregate +- Stream Aggregate + +#### Hash Aggregate 示例 + +TiDB 上的 Hash Aggregation 算子采用多线程并发优化,执行速度快,但会消耗较多内存。下面是一个 Hash Aggregate 的例子: + +``` +TiDB(root@127.0.0.1:test) > explain select /*+ HASH_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 示例 + +TiDB Stream Aggregation 算子通常会比 Hash Aggregate 占用更少的内存,有些场景中也会比 Hash Aggregate 执行得更快。当数据量太大或者系统内存不足时,可以试试 Stream Aggregate 算子。一个 Stream Aggregate 的例子如下: + ``` -+------------------------+---------+------+--------------------------------------------------------------------------------------------------+ -| id | count | task | operator info | -+------------------------+---------+------+--------------------------------------------------------------------------------------------------+ -| StreamAgg_25 | 1.00 | root | funcs:count(col_0) | -| └─IndexReader_26 | 1.00 | root | index:StreamAgg_9 | -| └─StreamAgg_9 | 1.00 | cop | funcs:count(1) | -| └─IndexScan_24 | 8166.73 | cop | table:trips, index:start_date, range:[2017-07-01 00:00:00,2017-07-01 23:59:59], keep order:false | -+------------------------+---------+------+--------------------------------------------------------------------------------------------------+ -4 rows in set (0.01 sec) +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) ``` -在添加完索引后的新执行计划中,使用 `IndexScan_24` 直接读取满足条件 `start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59'` 的数据,可以看到,估算的要扫描的数据行数从之前的 `19117643.00` 降到了现在的 `8166.73`。在测试环境中显示,这个查询的执行时间从 50.41 秒降到了 0.01 秒! +和 Hash Aggregate 类似,一般而言 TiDB 的 Stream Aggregate 也会分成两个阶段执行,一个在 TiKV/TiFlash 的 Coprocessor 上,在扫表算子读取数据时计算聚合函数的中间结果。另一个在 TiDB 层,汇总所有 Coprocessor Task 的中间结果后,得到最终结果。 -## `EXPLAIN FOR CONNECTION` +### 如何阅读 Join 的执行计划 -`EXPLAIN FOR CONNECTION` 用于获得一个连接中最后执行的查询的执行计划,其输出格式与 `EXPLAIN` 完全一致。但 TiDB 中的实现与 MySQL 不同,除了输出格式之外,还有以下区别: +TiDB 的 Join 算法包括如下几类: -- MySQL 返回的是**正在执行**的查询计划,而 TiDB 返回的是**最后执行**的查询计划。 -- MySQL 的文档中指出,MySQL 要求登录用户与被查询的连接相同,或者拥有 `PROCESS` 权限,而 TiDB 则要求登录用户与被查询的连接相同,或者拥有 `SUPER` 权限。 +- Hash Join +- Merge Join +- Index Join (Index Nested Loop Join) +- Index Hash Join (Index Nested Loop Hash Join) +- Index Merge Join (Index Nested Loop Merge Join) -## 概述 +下面分别通过一些例子来解释这些 Join 算法的执行过程。 -### Task 简介 +#### Hash Join 示例 -目前 TiDB 的计算任务隶属于两种不同的 task:cop task 和 root task。cop task 是指使用 TiKV 中的 coprocessor 执行的计算任务,root task 是指在 TiDB 中执行的计算任务。 +TiDB 的 Hash Join 算子采用了多线程优化,执行速度较快,但会消耗较多内存。一个 Hash Join 的例子如下: -SQL 优化的目标之一是将计算尽可能地下推到 TiKV 中执行。TiKV 中的 coprocessor 能支持大部分 SQL 内建函数(包括聚合函数和标量函数)、SQL `LIMIT` 操作、索引扫描和表扫描。但是,所有的 Join 操作都只能作为 root task 在 TiDB 上执行。 +``` +mysql> EXPLAIN SELECT /*+ HASH_JOIN(t1, t2) */ * FROM t1, t2 WHERE t1.id = t2.id; ++-----------------------------+----------+-----------+------------------------+------------------------------------------------+ +| id | estRows | task | access object | operator info | ++-----------------------------+----------+-----------+------------------------+------------------------------------------------+ +| HashJoin_30 | 12487.50 | root | | inner join, equal:[eq(test.t1.id, test.t2.id)] | +| ├─IndexReader_35(Build) | 9990.00 | root | | index:IndexFullScan_34 | +| │ └─IndexFullScan_34 | 9990.00 | cop[tikv] | table:t2, index:id(id) | keep order:false, stats:pseudo | +| └─IndexReader_33(Probe) | 9990.00 | root | | index:IndexFullScan_32 | +| └─IndexFullScan_32 | 9990.00 | cop[tikv] | table:t1, index:id(id) | keep order:false, stats:pseudo | ++-----------------------------+----------+-----------+------------------------+------------------------------------------------+ +5 rows in set (0.01 sec) +``` -### 表数据和索引数据 +Hash Join 会将 Build 端的数据缓存在内存中,根据这些数据构造出一个 Hash Table,然后读取 Probe 端的数据,用 Probe 端的数据去探测 Build 端构造出来的 Hash Table,将符合条件的数据返回给用户。 -TiDB 的表数据是指一张表的原始数据,存放在 TiKV 中。对于每行表数据,它的 key 是一个 64 位整数,称为 Handle ID。如果一张表存在 int 类型的主键,TiDB 会把主键的值当作表数据的 Handle ID,否则由系统自动生成 Handle ID。表数据的 value 由这一行的所有数据编码而成。在读取表数据的时候,可以按照 Handle ID 递增的顺序返回。 +#### Merge Join 示例 -TiDB 的索引数据和表数据一样,也存放在 TiKV 中。它的 key 是由索引列编码的有序 bytes,value 是这一行索引数据对应的 Handle ID,通过 Handle ID 可以读取这一行的非索引列。在读取索引数据的时候,TiKV 会按照索引列递增的顺序返回,如果有多个索引列,首先保证第 1 列递增,并且在第 i 列相等的情况下,保证第 i + 1 列递增。 +TiDB 的 Merge Join 算子相比于 Hash Join 通常会占用更少的内存,但可能执行时间会更久。当数据量太大,或系统内存不足时,建议尝试使用。下面是一个 Merge Join 的例子: -### 范围查询 +``` +mysql> EXPLAIN SELECT /*+ MERGE_JOIN(t1, t2) */ * FROM t1, t2 WHERE t1.id = t2.id; ++-----------------------------+----------+-----------+------------------------+-------------------------------------------------------+ +| id | estRows | task | access object | operator info | ++-----------------------------+----------+-----------+------------------------+-------------------------------------------------------+ +| MergeJoin_7 | 12487.50 | root | | inner join, left key:test.t1.id, right key:test.t2.id | +| ├─IndexReader_12(Build) | 9990.00 | root | | index:IndexFullScan_11 | +| │ └─IndexFullScan_11 | 9990.00 | cop[tikv] | table:t2, index:id(id) | keep order:true, stats:pseudo | +| └─IndexReader_10(Probe) | 9990.00 | root | | index:IndexFullScan_9 | +| └─IndexFullScan_9 | 9990.00 | cop[tikv] | table:t1, index:id(id) | keep order:true, stats:pseudo | ++-----------------------------+----------+-----------+------------------------+-------------------------------------------------------+ +5 rows in set (0.01 sec) +``` -在 WHERE/HAVING/ON 条件中,TiDB 优化器会分析主键或索引键的查询返回。如数字、日期类型的比较符,如大于、小于、等于以及大于等于、小于等于,字符类型的 LIKE 符号等。 +Merge Join 算子在执行时,会从 Build 端把一个 Join Group 的数据全部读取到内存中,接着再去读 Probe 端的数据,用 Probe 端的每行数据去和 Build 端的一个完整 Join Group 比较,依次查看是否匹配(除了满足等值条件以外,还有其他非等值条件,这里的 “匹配” 主要是指查看是否满足非等值条件)。Join Group 指的是所有 Join Key 上值相同的数据。 + +#### Index Join 示例 + +对于外表经过 WHERE 条件过滤后结果集较小(小于 1 万行)的场景,可以尝试使用这个算法。 + +``` +mysql> EXPLAIN SELECT /*+ INL_JOIN(t1, t2) */ * FROM t1, t2 WHERE t1.id = t2.id; ++-----------------------------+----------+-----------+------------------------+--------------------------------------------------------------------------------+ +| id | estRows | task | access object | operator info | ++-----------------------------+----------+-----------+------------------------+--------------------------------------------------------------------------------+ +| IndexJoin_11 | 12487.50 | root | | inner join, inner:IndexReader_10, outer key:test.t1.id, inner key:test.t2.id | +| ├─IndexReader_31(Build) | 9990.00 | root | | index:IndexFullScan_30 | +| │ └─IndexFullScan_30 | 9990.00 | cop[tikv] | table:t1, index:id(id) | keep order:false, stats:pseudo | +| └─IndexReader_10(Probe) | 1.00 | root | | index:Selection_9 | +| └─Selection_9 | 1.00 | cop[tikv] | | not(isnull(test.t2.id)) | +| └─IndexRangeScan_8 | 1.00 | cop[tikv] | table:t2, index:id(id) | range: decided by [eq(test.t2.id, test.t1.id)], keep order:false, stats:pseudo | ++-----------------------------+----------+-----------+------------------------+--------------------------------------------------------------------------------+ +6 rows in set (0.00 sec) +``` -值得注意的是,TiDB 目前只支持比较符一端是列,另一端是常量,或可以计算成某一常量的情况,类似 `year(birth_day) < 1992` 的查询条件是不能利用索引的。还要注意应尽可能使用同一类型进行比较,以避免引入额外的 cast 操作而导致不能利用索引,如 `user_id = 123456`,如果 `user_id` 是字符串,需要将 `123456` 也写成字符串常量的形式。 +#### Index Hash Join 示例 -针对同一列的范围查询条件使用 `AND` 和 `OR` 组合后,等于对范围求交集或者并集。对于多维组合索引,可以写多个列的条件。例如对组合索引`(a, b, c)`,当 a 为等值查询时,可以继续求 b 的查询范围,当 b 也为等值查询时,可以继续求 c 的查询范围,反之如果 a 为非等值查询,则只能求 a 的范围。 +该算法与 Index Join 使用条件完全一样,但在某些场景下会更为节省内存资源。 -## Operator Info +``` +mysql> EXPLAIN SELECT /*+ INL_HASH_JOIN(t1, t2) */ * FROM t1, t2 WHERE t1.id = t2.id; ++-----------------------------+----------+-----------+------------------------+--------------------------------------------------------------------------------+ +| id | estRows | task | access object | operator info | ++-----------------------------+----------+-----------+------------------------+--------------------------------------------------------------------------------+ +| IndexHashJoin_18 | 12487.50 | root | | inner join, inner:IndexReader_10, outer key:test.t1.id, inner key:test.t2.id | +| ├─IndexReader_31(Build) | 9990.00 | root | | index:IndexFullScan_30 | +| │ └─IndexFullScan_30 | 9990.00 | cop[tikv] | table:t1, index:id(id) | keep order:false, stats:pseudo | +| └─IndexReader_10(Probe) | 1.00 | root | | index:Selection_9 | +| └─Selection_9 | 1.00 | cop[tikv] | | not(isnull(test.t2.id)) | +| └─IndexRangeScan_8 | 1.00 | cop[tikv] | table:t2, index:id(id) | range: decided by [eq(test.t2.id, test.t1.id)], keep order:false, stats:pseudo | ++-----------------------------+----------+-----------+------------------------+--------------------------------------------------------------------------------+ +6 rows in set (0.00 sec) +``` -### TableReader 和 TableScan +#### Index Merge Join 示例 -TableScan 表示在 KV 端对表数据进行扫描,TableReader 表示在 TiDB 端从 TiKV 端读取,属于同一功能的两个算子。table 表示 SQL 语句中的表名,如果表名被重命名,则显示重命名。range 表示扫描的数据范围,如果在查询中不指定 WHERE/HAVING/ON 条件,则会选择全表扫描,如果在 int 类型的主键上有范围查询条件,会选择范围查询。keep order 表示 table scan 是否按顺序返回。 +该算法的使用条件包含 Index Join 的所有使用条件,但还需要添加一条:join keys 中的内表列集合是内表使用的 index 的前缀,或内表使用的 index 是 join keys 中的内表列集合的前缀,该算法相比于 INL_JOIN 会更节省内存。 -### IndexReader 和 IndexLookUp +``` +mysql> EXPLAIN SELECT /*+ INL_MERGE_JOIN(t1, t2) */ * FROM t1, t2 WHERE t1.id = t2.id; ++-----------------------------+----------+-----------+------------------------+-------------------------------------------------------------------------------+ +| id | estRows | task | access object | operator info | ++-----------------------------+----------+-----------+------------------------+-------------------------------------------------------------------------------+ +| IndexMergeJoin_16 | 12487.50 | root | | inner join, inner:IndexReader_14, outer key:test.t1.id, inner key:test.t2.id | +| ├─IndexReader_31(Build) | 9990.00 | root | | index:IndexFullScan_30 | +| │ └─IndexFullScan_30 | 9990.00 | cop[tikv] | table:t1, index:id(id) | keep order:false, stats:pseudo | +| └─IndexReader_14(Probe) | 1.00 | root | | index:Selection_13 | +| └─Selection_13 | 1.00 | cop[tikv] | | not(isnull(test.t2.id)) | +| └─IndexRangeScan_12 | 1.00 | cop[tikv] | table:t2, index:id(id) | range: decided by [eq(test.t2.id, test.t1.id)], keep order:true, stats:pseudo | ++-----------------------------+----------+-----------+------------------------+-------------------------------------------------------------------------------+ +6 rows in set (0.00 sec) +``` -Index 在 TiDB 端的读取方式有两种:IndexReader 表示直接从索引中读取索引列,适用于 SQL 语句中仅引用了该索引相关的列或主键;IndexLookUp 表示从索引中过滤部分数据,仅返回这些数据的 Handle ID,通过 Handle ID 再次查找表数据,这种方式需要两次从 TiKV 获取数据。Index 的读取方式是由优化器自动选择的。 +## 优化实例 -IndexScan 是 KV 端读取索引数据的算子,和 TableScan 功能类似。table 表示 SQL 语句中的表名,如果表名被重命名,则显示重命名。index 表示索引名。range 表示扫描的数据范围。out of order 表示 index scan 是否按照顺序返回。注意在 TiDB 中,多列或者非 int 列构成的主键是当作唯一索引处理的。 +使用 [bikeshare example database](https://pingcap.com/docs/dev/how-to/get-started/import-example-database/): -### Selection +{{< copyable "sql" >}} -Selection 表示 SQL 语句中的选择条件,通常出现在 WHERE/HAVING/ON 子句中。 +```sql +EXPLAIN SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59'; +``` + +``` ++------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+ +| id | estRows | task | access object | operator info | ++------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+ +| StreamAgg_20 | 1.00 | root | | funcs:count(Column#13)->Column#11 | +| └─TableReader_21 | 1.00 | root | | data:StreamAgg_9 | +| └─StreamAgg_9 | 1.00 | cop[tikv] | | funcs:count(1)->Column#13 | +| └─Selection_19 | 8166.73 | cop[tikv] | | ge(bikeshare.trips.start_date, 2017-07-01 00:00:00.000000), le(bikeshare.trips.start_date, 2017-07-01 23:59:59.000000) | +| └─TableFullScan_18 | 19117643.00 | cop[tikv] | table:trips | keep order:false, stats:pseudo | ++------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+ +``` -### Projection +在上面的例子中,coprocessor 上读取 trips 表上的数据 (`TableScan_18`),寻找满足 `start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59'` 条件的数据 (`Selection_19`),然后计算满足条件的数据行数 (`StreamAgg_9`),最后把结果返回给 TiDB。TiDB 汇总各个 coprocessor 返回的结果 (`TableReader_21`),并进一步计算所有数据的行数 (`StreamAgg_20`),最终把结果返回给客户端。在上面这个查询中,TiDB 根据 `trips` 表的统计信息估算出 `TableScan_18` 的输出结果行数为 19117643.00,满足条件 `start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59'` 的有 8166.73 条,经过聚合运算后,只有 1 条结果。 -Projection 对应 SQL 语句中的 SELECT 列表,功能是将每一条输入数据映射成新的输出数据。 +上述查询中,虽然大部分计算逻辑都下推到了 TiKV 的 coprocessor 上,但是其执行效率还是不够高,可以添加适当的索引来消除 `TableScan_18` 对 trips 的全表扫,进一步加速查询的执行: -### Aggregation +{{< copyable "sql" >}} -Aggregation 对应 SQL 语句中的 Group By 语句或者没有 Group By 语句但是存在聚合函数,例如 count 或 sum 函数等。TiDB 支持两种聚合算法:Hash Aggregation 以及 Stream Aggregation(待补充)。Hash Aggregation 是基于哈希的聚合算法,如果 Hash Aggregation 紧邻 Table 或者 Index 的读取算子,则聚合算子会在 TiKV 端进行预聚合,以提高计算的并行度和减少网络开销。 +```sql +ALTER TABLE trips ADD INDEX (start_date); +``` -### Join +{{< copyable "sql" >}} -TiDB 支持 Inner Join 以及 Left/Right Outer Join,并会自动将可以化简的外连接转换为 Inner Join。 +```sql +EXPLAIN SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59'; +``` -TiDB 支持三种 Join 算法:Hash Join,Sort Merge Join 和 Index Look up Join。Hash Join 的原理是将参与连接的小表预先装载到内存中,读取大表的所有数据进行连接。Sort Merge Join 会利用输入数据的有序信息,同时读取两张表的数据并依次进行比较。Index Look Up Join 会读取外表的数据,并对内表进行主键或索引键查询。 +``` ++-----------------------------+---------+-----------+-------------------------------------------+---------------------------------------------------------------------------------+ +| id | estRows | task | access object | operator info | ++-----------------------------+---------+-----------+-------------------------------------------+---------------------------------------------------------------------------------+ +| StreamAgg_17 | 1.00 | root | | funcs:count(Column#13)->Column#11 | +| └─IndexReader_18 | 1.00 | root | | index:StreamAgg_9 | +| └─StreamAgg_9 | 1.00 | cop[tikv] | | funcs:count(1)->Column#13 | +| └─IndexRangeScan_16 | 8166.73 | cop[tikv] | table:trips, index:start_date(start_date) | range:[2017-07-01 00:00:00,2017-07-01 23:59:59], keep order:false, stats:pseudo | ++-----------------------------+---------+-----------+-------------------------------------------+---------------------------------------------------------------------------------+ +4 rows in set (0.00 sec) +``` -### Apply +在添加完索引后的新执行计划中,使用 `IndexScan_24` 直接读取满足条件 `start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59'` 的数据,可以看到,估算的要扫描的数据行数从之前的 19117643.00 降到了现在的 8166.73。在测试环境中显示,这个查询的执行时间从 50.41 秒降到了 0.01 秒! -Apply 是 TiDB 用来描述子查询的一种算子,行为类似于 Nested Loop,即每次从外表中取一条数据,带入到内表的关联列中,并执行,最后根据 Apply 内联的 Join 算法进行连接计算。 +## 另请参阅 -值得注意的是,Apply 一般会被查询优化器自动转换为 Join 操作。用户在编写 SQL 的过程中应尽量避免 Apply 算子的出现。 +* [EXPLAIN](/reference/sql/statements/explain.md) +* [EXPLAIN ANALYZE](/reference/sql/statements/explain-analyze.md) +* [ANALYZE TABLE](/reference/sql/statements/analyze-table.md) +* [TRACE](/reference/sql/statements/trace.md) +* [TiDB in Action](https://book.tidb.io/session3/chapter1/sql-execution-plan.html) diff --git a/reference/sql/statements/add-index.md b/reference/sql/statements/add-index.md index 94d971b1aa93..ffe23024b165 100644 --- a/reference/sql/statements/add-index.md +++ b/reference/sql/statements/add-index.md @@ -60,13 +60,13 @@ EXPLAIN SELECT * FROM t1 WHERE c1 = 3; ``` ``` -+---------------------+----------+------+-------------------------------------------------------------+ -| id | count | task | operator info | -+---------------------+----------+------+-------------------------------------------------------------+ -| TableReader_7 | 10.00 | root | data:Selection_6 | -| └─Selection_6 | 10.00 | cop | eq(test.t1.c1, 3) | -| └─TableScan_5 | 10000.00 | cop | table:t1, range:[-inf,+inf], keep order:false, stats:pseudo | -+---------------------+----------+------+-------------------------------------------------------------+ ++-------------------------+----------+-----------+---------------+--------------------------------+ +| id | estRows | task | access object | operator info | ++-------------------------+----------+-----------+---------------+--------------------------------+ +| TableReader_7 | 10.00 | root | | data:Selection_6 | +| └─Selection_6 | 10.00 | cop[tikv] | | eq(test.t1.c1, 3) | +| └─TableFullScan_5 | 10000.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | ++-------------------------+----------+-----------+---------------+--------------------------------+ 3 rows in set (0.00 sec) ``` @@ -87,12 +87,12 @@ EXPLAIN SELECT * FROM t1 WHERE c1 = 3; ``` ``` -+-------------------+-------+------+-----------------------------------------------------------------+ -| id | count | task | operator info | -+-------------------+-------+------+-----------------------------------------------------------------+ -| IndexReader_6 | 10.00 | root | index:IndexScan_5 | -| └─IndexScan_5 | 10.00 | cop | table:t1, index:c1, range:[3,3], keep order:false, stats:pseudo | -+-------------------+-------+------+-----------------------------------------------------------------+ ++------------------------+---------+-----------+------------------------+---------------------------------------------+ +| id | estRows | task | access object | operator info | ++------------------------+---------+-----------+------------------------+---------------------------------------------+ +| IndexReader_6 | 0.01 | root | | index:IndexRangeScan_5 | +| └─IndexRangeScan_5 | 0.01 | cop[tikv] | table:t1, index:c1(c1) | range:[3,3], keep order:false, stats:pseudo | ++------------------------+---------+-----------+------------------------+---------------------------------------------+ 2 rows in set (0.00 sec) ``` diff --git a/reference/sql/statements/alter-table.md b/reference/sql/statements/alter-table.md index d438f7d2bf68..0a4756e207e7 100644 --- a/reference/sql/statements/alter-table.md +++ b/reference/sql/statements/alter-table.md @@ -55,13 +55,13 @@ EXPLAIN SELECT * FROM t1 WHERE c1 = 3; ``` ``` -+---------------------+----------+------+-------------------------------------------------------------+ -| id | count | task | operator info | -+---------------------+----------+------+-------------------------------------------------------------+ -| TableReader_7 | 10.00 | root | data:Selection_6 | -| └─Selection_6 | 10.00 | cop | eq(test.t1.c1, 3) | -| └─TableScan_5 | 10000.00 | cop | table:t1, range:[-inf,+inf], keep order:false, stats:pseudo | -+---------------------+----------+------+-------------------------------------------------------------+ ++-------------------------+----------+-----------+---------------+--------------------------------+ +| id | estRows | task | access object | operator info | ++-------------------------+----------+-----------+---------------+--------------------------------+ +| TableReader_7 | 10.00 | root | | data:Selection_6 | +| └─Selection_6 | 10.00 | cop[tikv] | | eq(test.t1.c1, 3) | +| └─TableFullScan_5 | 10000.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | ++-------------------------+----------+-----------+---------------+--------------------------------+ 3 rows in set (0.00 sec) ``` @@ -82,12 +82,12 @@ EXPLAIN SELECT * FROM t1 WHERE c1 = 3; ``` ``` -+-------------------+-------+------+-----------------------------------------------------------------+ -| id | count | task | operator info | -+-------------------+-------+------+-----------------------------------------------------------------+ -| IndexReader_6 | 10.00 | root | index:IndexScan_5 | -| └─IndexScan_5 | 10.00 | cop | table:t1, index:c1, range:[3,3], keep order:false, stats:pseudo | -+-------------------+-------+------+-----------------------------------------------------------------+ ++------------------------+---------+-----------+------------------------+---------------------------------------------+ +| id | estRows | task | access object | operator info | ++------------------------+---------+-----------+------------------------+---------------------------------------------+ +| IndexReader_6 | 10.00 | root | | index:IndexRangeScan_5 | +| └─IndexRangeScan_5 | 10.00 | cop[tikv] | table:t1, index:c1(c1) | range:[3,3], keep order:false, stats:pseudo | ++------------------------+---------+-----------+------------------------+---------------------------------------------+ 2 rows in set (0.00 sec) ``` diff --git a/reference/sql/statements/analyze-table.md b/reference/sql/statements/analyze-table.md index e44827a42477..48a23d97ceac 100644 --- a/reference/sql/statements/analyze-table.md +++ b/reference/sql/statements/analyze-table.md @@ -64,12 +64,12 @@ EXPLAIN SELECT * FROM t1 WHERE c1 = 3; ``` ``` -+-------------------+-------+------+-----------------------------------------------------------------+ -| id | count | task | operator info | -+-------------------+-------+------+-----------------------------------------------------------------+ -| IndexReader_6 | 10.00 | root | index:IndexScan_5 | -| └─IndexScan_5 | 10.00 | cop | table:t1, index:c1, range:[3,3], keep order:false, stats:pseudo | -+-------------------+-------+------+-----------------------------------------------------------------+ ++------------------------+---------+-----------+------------------------+---------------------------------------------+ +| id | estRows | task | access object | operator info | ++------------------------+---------+-----------+------------------------+---------------------------------------------+ +| IndexReader_6 | 10.00 | root | | index:IndexRangeScan_5 | +| └─IndexRangeScan_5 | 10.00 | cop[tikv] | table:t1, index:c1(c1) | range:[3,3], keep order:false, stats:pseudo | ++------------------------+---------+-----------+------------------------+---------------------------------------------+ 2 rows in set (0.00 sec) ``` @@ -90,12 +90,12 @@ EXPLAIN SELECT * FROM t1 WHERE c1 = 3; ``` ``` -+-------------------+-------+------+---------------------------------------------------+ -| id | count | task | operator info | -+-------------------+-------+------+---------------------------------------------------+ -| IndexReader_6 | 1.00 | root | index:IndexScan_5 | -| └─IndexScan_5 | 1.00 | cop | table:t1, index:c1, range:[3,3], keep order:false | -+-------------------+-------+------+---------------------------------------------------+ ++------------------------+---------+-----------+------------------------+-------------------------------+ +| id | estRows | task | access object | operator info | ++------------------------+---------+-----------+------------------------+-------------------------------+ +| IndexReader_6 | 1.00 | root | | index:IndexRangeScan_5 | +| └─IndexRangeScan_5 | 1.00 | cop[tikv] | table:t1, index:c1(c1) | range:[3,3], keep order:false | ++------------------------+---------+-----------+------------------------+-------------------------------+ 2 rows in set (0.00 sec) ``` diff --git a/reference/sql/statements/create-index.md b/reference/sql/statements/create-index.md index 652146e26d12..bd66e1657154 100644 --- a/reference/sql/statements/create-index.md +++ b/reference/sql/statements/create-index.md @@ -72,13 +72,13 @@ EXPLAIN SELECT * FROM t1 WHERE c1 = 3; ``` ``` -+---------------------+----------+------+-------------------------------------------------------------+ -| id | count | task | operator info | -+---------------------+----------+------+-------------------------------------------------------------+ -| TableReader_7 | 10.00 | root | data:Selection_6 | -| └─Selection_6 | 10.00 | cop | eq(test.t1.c1, 3) | -| └─TableScan_5 | 10000.00 | cop | table:t1, range:[-inf,+inf], keep order:false, stats:pseudo | -+---------------------+----------+------+-------------------------------------------------------------+ ++-------------------------+----------+-----------+---------------+--------------------------------+ +| id | estRows | task | access object | operator info | ++-------------------------+----------+-----------+---------------+--------------------------------+ +| TableReader_7 | 10.00 | root | | data:Selection_6 | +| └─Selection_6 | 10.00 | cop[tikv] | | eq(test.t1.c1, 3) | +| └─TableFullScan_5 | 10000.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | ++-------------------------+----------+-----------+---------------+--------------------------------+ 3 rows in set (0.00 sec) ``` @@ -99,12 +99,12 @@ EXPLAIN SELECT * FROM t1 WHERE c1 = 3; ``` ``` -+-------------------+-------+------+-----------------------------------------------------------------+ -| id | count | task | operator info | -+-------------------+-------+------+-----------------------------------------------------------------+ -| IndexReader_6 | 10.00 | root | index:IndexScan_5 | -| └─IndexScan_5 | 10.00 | cop | table:t1, index:c1, range:[3,3], keep order:false, stats:pseudo | -+-------------------+-------+------+-----------------------------------------------------------------+ ++------------------------+---------+-----------+------------------------+---------------------------------------------+ +| id | estRows | task | access object | operator info | ++------------------------+---------+-----------+------------------------+---------------------------------------------+ +| IndexReader_6 | 10.00 | root | | index:IndexRangeScan_5 | +| └─IndexRangeScan_5 | 10.00 | cop[tikv] | table:t1, index:c1(c1) | range:[3,3], keep order:false, stats:pseudo | ++------------------------+---------+-----------+------------------------+---------------------------------------------+ 2 rows in set (0.00 sec) ``` diff --git a/reference/sql/statements/drop-index.md b/reference/sql/statements/drop-index.md index 1b4b35ce2151..5e09d998f926 100644 --- a/reference/sql/statements/drop-index.md +++ b/reference/sql/statements/drop-index.md @@ -56,13 +56,13 @@ EXPLAIN SELECT * FROM t1 WHERE c1 = 3; ``` ``` -+---------------------+----------+------+-------------------------------------------------------------+ -| id | count | task | operator info | -+---------------------+----------+------+-------------------------------------------------------------+ -| TableReader_7 | 10.00 | root | data:Selection_6 | -| └─Selection_6 | 10.00 | cop | eq(test.t1.c1, 3) | -| └─TableScan_5 | 10000.00 | cop | table:t1, range:[-inf,+inf], keep order:false, stats:pseudo | -+---------------------+----------+------+-------------------------------------------------------------+ ++-------------------------+----------+-----------+---------------+--------------------------------+ +| id | estRows | task | access object | operator info | ++-------------------------+----------+-----------+---------------+--------------------------------+ +| TableReader_7 | 10.00 | root | | data:Selection_6 | +| └─Selection_6 | 10.00 | cop[tikv] | | eq(test.t1.c1, 3) | +| └─TableFullScan_5 | 10000.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | ++-------------------------+----------+-----------+---------------+--------------------------------+ 3 rows in set (0.00 sec) ``` @@ -83,12 +83,12 @@ EXPLAIN SELECT * FROM t1 WHERE c1 = 3; ``` ``` -+-------------------+-------+------+-----------------------------------------------------------------+ -| id | count | task | operator info | -+-------------------+-------+------+-----------------------------------------------------------------+ -| IndexReader_6 | 10.00 | root | index:IndexScan_5 | -| └─IndexScan_5 | 10.00 | cop | table:t1, index:c1, range:[3,3], keep order:false, stats:pseudo | -+-------------------+-------+------+-----------------------------------------------------------------+ ++------------------------+---------+-----------+------------------------+---------------------------------------------+ +| id | estRows | task | access object | operator info | ++------------------------+---------+-----------+------------------------+---------------------------------------------+ +| IndexReader_6 | 0.01 | root | | index:IndexRangeScan_5 | +| └─IndexRangeScan_5 | 0.01 | cop[tikv] | table:t1, index:c1(c1) | range:[3,3], keep order:false, stats:pseudo | ++------------------------+---------+-----------+------------------------+---------------------------------------------+ 2 rows in set (0.00 sec) ``` diff --git a/reference/sql/statements/explain-analyze.md b/reference/sql/statements/explain-analyze.md index c187a7eebe66..333c5033ecc9 100644 --- a/reference/sql/statements/explain-analyze.md +++ b/reference/sql/statements/explain-analyze.md @@ -22,6 +22,17 @@ category: reference ![ExplainableStmt](/media/sqlgram/ExplainableStmt.png) +## EXPLAIN ANALYZE 输出格式 + +和 `EXPLAIN` 不同,`EXPLAIN ANALYZE` 会执行对应的 SQL 语句,记录其运行时信息,和执行计划一并返回出来。因此,可以将 `EXPLAIN ANALYZE` 视为 `EXPLAIN` 语句的扩展。`EXPLAIN ANALYZE` 语句的返回结果相比 `EXPLAIN`,增加了 `actRows`,`execution info`,`memory`,`disk` 这几列信息: + +| 属性名 | 含义 | +|:----------------|:---------------------------------| +| actRows | 算子实际输出的数据条数。 | +| execution info | 算子的实际执行信息。time 表示从进入算子到离开算子的全部 wall time,包括所有子算子操作的全部执行时间。如果该算子被父算子多次调用 (loops),这个时间就是累积的时间。loops 是当前算子被父算子调用的次数。 | +| memory | 算子占用内存空间的大小。 | +| disk | 算子占用磁盘空间的大小。 | + ## 示例 {{< copyable "sql" >}} @@ -52,11 +63,11 @@ EXPLAIN ANALYZE SELECT * FROM t1 WHERE id = 1; ``` ``` -+-------------+-------+------+--------------------+---------------------------+ -| id | count | task | operator info | execution info | -+-------------+-------+------+--------------------+---------------------------+ -| Point_Get_1 | 1.00 | root | table:t1, handle:1 | time:0ns, loops:0, rows:0 | -+-------------+-------+------+--------------------+---------------------------+ ++-------------+---------+---------+------+---------------+--------------------------+---------------+--------+------+ +| id | estRows | actRows | task | access object | execution info | operator info | memory | disk | ++-------------+---------+---------+------+---------------+--------------------------+---------------+--------+------+ +| Point_Get_1 | 1.00 | 1 | root | table:t1 | time:177.183µs, loops:2 | handle:1 | N/A | N/A | ++-------------+---------+---------+------+---------------+--------------------------+---------------+--------+------+ 1 row in set (0.01 sec) ``` @@ -67,12 +78,12 @@ EXPLAIN ANALYZE SELECT * FROM t1; ``` ``` -+-----------------------+----------+-----------+------------------------------------------+-------------------------------------------------------------------------------+-----------+------+ -| id | count | task | operator info | execution info | memory | disk | -+-----------------------+----------+-----------+------------------------------------------+-------------------------------------------------------------------------------+-----------+------+ -| TableReader_5 | 10000.00 | root | data:TableFullScan_4 | time:148.128µs, loops:2, rows:3, rpc num: 1, rpc time:97.812µs, proc keys:0 | 199 Bytes | N/A | -| └─TableFullScan_4 | 10000.00 | cop[tikv] | table:t1, keep order:false, stats:pseudo | time:40.918µs, loops:4, rows:3 | N/A | N/A | -+-----------------------+----------+-----------+------------------------------------------+-------------------------------------------------------------------------------+-----------+------+ ++-----------------------+----------+---------+-----------+---------------+------------------------------------------------------------------------+--------------------------------+-----------+------+ +| id | estRows | actRows | task | access object | execution info | operator info | memory | disk | ++-----------------------+----------+---------+-----------+---------------+------------------------------------------------------------------------+--------------------------------+-----------+------+ +| TableReader_5 | 10000.00 | 3 | root | | time:454.744µs, loops:2, rpc num: 1, rpc time:328.334µs, proc keys:0 | data:TableFullScan_4 | 199 Bytes | N/A | +| └─TableFullScan_4 | 10000.00 | 3 | cop[tikv] | table:t1 | time:148.227µs, loops:4 | keep order:false, stats:pseudo | N/A | N/A | ++-----------------------+----------+---------+-----------+---------------+------------------------------------------------------------------------+--------------------------------+-----------+------+ 2 rows in set (0.00 sec) ``` diff --git a/reference/sql/statements/explain.md b/reference/sql/statements/explain.md index e5926e18e218..fb6266d73ee3 100644 --- a/reference/sql/statements/explain.md +++ b/reference/sql/statements/explain.md @@ -10,6 +10,8 @@ category: reference 语句 `DESC` 和 `DESCRIBE` 是 `EXPLAIN` 的别名。`EXPLAIN ` 的替代用法记录在 [`SHOW [FULL] COLUMNS FROM`](/reference/sql/statements/show-columns-from.md) 下。 +TiDB 支持 `EXPLAIN [options] FOR CONNECTION connection_id`,但与 MySQL 的 `EXPLAIN FOR` 有一些区别,请参见 [`EXPLAIN FOR CONNECTION`](#explain-for-connection)。 + ## 语法图 **ExplainSym:** @@ -24,6 +26,18 @@ category: reference ![ExplainableStmt](/media/sqlgram/ExplainableStmt.png) +## EXPLAIN 输出格式 + +目前 TiDB 的 `EXPLAIN` 会输出 5 列,分别是:`id`,`estRows`,`task`,`access object`, `operator info`。执行计划中每个算子都由这 5 列属性来描述,`EXPLAIN`结果中每一行描述一个算子。每个属性的具体含义如下: + +| 属性名 | 含义 | +|:----------------|:----------------------------------------------------------------------------------------------------------| +| id | 算子的 ID,是算子在整个执行计划中唯一的标识。在 TiDB 2.1 中,ID 会格式化地显示算子的树状结构。数据从孩子结点流向父亲结点,每个算子的父亲结点有且仅有一个。| +| estRows | 算子预计将会输出的数据条数,基于统计信息以及算子的执行逻辑估算而来。在 4.0 之前叫 count。 | +| task | 算子属于的 task 种类。目前的执行计划分成为两种 task,一种叫 **root** task,在 tidb-server 上执行,一种叫 **cop** task,在 TiKV 或者 TiFlash 上并行执行。当前的执行计划在 task 级别的拓扑关系是一个 root task 后面可以跟许多 cop task,root task 使用 cop task 的输出结果作为输入。cop task 中执行的也即是 TiDB 下推到 TiKV 或者 TiFlash 上的任务,每个 cop task 分散在 TiKV 或者 TiFlash 集群中,由多个进程共同执行。 | +| access object | 算子所访问的数据项信息。包括表 `table`,表分区 `partition` 以及使用的索引 `index`(如果有)。只有直接访问数据的算子才拥有这些信息。 | +| operator info | 算子的其它信息。各个算子的 operator info 各有不同,可参考下面的示例解读。 | + ## 示例 {{< copyable "sql" >}} @@ -33,12 +47,12 @@ EXPLAIN SELECT 1; ``` ``` -+-------------------+-------+------+---------------+ -| id | count | task | operator info | -+-------------------+-------+------+---------------+ -| Projection_3 | 1.00 | root | 1 | -| └─TableDual_4 | 1.00 | root | rows:1 | -+-------------------+-------+------+---------------+ ++-------------------+---------+------+---------------+---------------+ +| id | estRows | task | access object | operator info | ++-------------------+---------+------+---------------+---------------+ +| Projection_3 | 1.00 | root | | 1->Column#1 | +| └─TableDual_4 | 1.00 | root | | rows:1 | ++-------------------+---------+------+---------------+---------------+ 2 rows in set (0.00 sec) ``` @@ -70,11 +84,11 @@ EXPLAIN SELECT * FROM t1 WHERE id = 1; ``` ``` -+-------------+-------+------+--------------------+ -| id | count | task | operator info | -+-------------+-------+------+--------------------+ -| Point_Get_1 | 1.00 | root | table:t1, handle:1 | -+-------------+-------+------+--------------------+ ++-------------+---------+------+---------------+---------------+ +| id | estRows | task | access object | operator info | ++-------------+---------+------+---------------+---------------+ +| Point_Get_1 | 1.00 | root | table:t1 | handle:1 | ++-------------+---------+------+---------------+---------------+ 1 row in set (0.00 sec) ``` @@ -85,11 +99,11 @@ DESC SELECT * FROM t1 WHERE id = 1; ``` ``` -+-------------+-------+------+--------------------+ -| id | count | task | operator info | -+-------------+-------+------+--------------------+ -| Point_Get_1 | 1.00 | root | table:t1, handle:1 | -+-------------+-------+------+--------------------+ ++-------------+---------+------+---------------+---------------+ +| id | estRows | task | access object | operator info | ++-------------+---------+------+---------------+---------------+ +| Point_Get_1 | 1.00 | root | table:t1 | handle:1 | ++-------------+---------+------+---------------+---------------+ 1 row in set (0.00 sec) ``` @@ -100,11 +114,11 @@ DESCRIBE SELECT * FROM t1 WHERE id = 1; ``` ``` -+-------------+-------+------+--------------------+ -| id | count | task | operator info | -+-------------+-------+------+--------------------+ -| Point_Get_1 | 1.00 | root | table:t1, handle:1 | -+-------------+-------+------+--------------------+ ++-------------+---------+------+---------------+---------------+ +| id | estRows | task | access object | operator info | ++-------------+---------+------+---------------+---------------+ +| Point_Get_1 | 1.00 | root | table:t1 | handle:1 | ++-------------+---------+------+---------------+---------------+ 1 row in set (0.00 sec) ``` @@ -115,7 +129,12 @@ EXPLAIN INSERT INTO t1 (c1) VALUES (4); ``` ``` -ERROR 1105 (HY000): Unsupported type *core.Insert ++----------+---------+------+---------------+---------------+ +| id | estRows | task | access object | operator info | ++----------+---------+------+---------------+---------------+ +| Insert_1 | N/A | root | | N/A | ++----------+---------+------+---------------+---------------+ +1 row in set (0.00 sec) ``` {{< copyable "sql" >}} @@ -125,14 +144,15 @@ EXPLAIN UPDATE t1 SET c1=5 WHERE c1=3; ``` ``` -+---------------------+----------+------+-------------------------------------------------------------+ -| id | count | task | operator info | -+---------------------+----------+------+-------------------------------------------------------------+ -| TableReader_6 | 10.00 | root | data:Selection_5 | -| └─Selection_5 | 10.00 | cop | eq(test.t1.c1, 3) | -| └─TableScan_4 | 10000.00 | cop | table:t1, range:[-inf,+inf], keep order:false, stats:pseudo | -+---------------------+----------+------+-------------------------------------------------------------+ -3 rows in set (0.00 sec) ++---------------------------+---------+-----------+---------------+--------------------------------+ +| id | estRows | task | access object | operator info | ++---------------------------+---------+-----------+---------------+--------------------------------+ +| Update_4 | N/A | root | | N/A | +| └─TableReader_8 | 0.00 | root | | data:Selection_7 | +| └─Selection_7 | 0.00 | cop[tikv] | | eq(test.t1.c1, 3) | +| └─TableFullScan_6 | 3.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | ++---------------------------+---------+-----------+---------------+--------------------------------+ +4 rows in set (0.00 sec) ``` {{< copyable "sql" >}} @@ -142,14 +162,15 @@ EXPLAIN DELETE FROM t1 WHERE c1=3; ``` ``` -+---------------------+----------+------+-------------------------------------------------------------+ -| id | count | task | operator info | -+---------------------+----------+------+-------------------------------------------------------------+ -| TableReader_6 | 10.00 | root | data:Selection_5 | -| └─Selection_5 | 10.00 | cop | eq(test.t1.c1, 3) | -| └─TableScan_4 | 10000.00 | cop | table:t1, range:[-inf,+inf], keep order:false, stats:pseudo | -+---------------------+----------+------+-------------------------------------------------------------+ -3 rows in set (0.00 sec) ++---------------------------+---------+-----------+---------------+--------------------------------+ +| id | estRows | task | access object | operator info | ++---------------------------+---------+-----------+---------------+--------------------------------+ +| Delete_4 | N/A | root | | N/A | +| └─TableReader_8 | 0.00 | root | | data:Selection_7 | +| └─Selection_7 | 0.00 | cop[tikv] | | eq(test.t1.c1, 3) | +| └─TableFullScan_6 | 3.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | ++---------------------------+---------+-----------+---------------+--------------------------------+ +4 rows in set (0.01 sec) ``` 如果未指定 `FORMAT`,或未指定 `FORMAT ="row"`,那么 `EXPLAIN` 语句将以表格格式输出结果。更多信息,可参阅 [Understand the Query Execution Plan](https://pingcap.com/docs/dev/reference/performance/understanding-the-query-execution-plan/)。 @@ -163,35 +184,38 @@ create table t(a bigint, b bigint); desc format = "dot" select A.a, B.b from t A join t B on A.a > B.b where A.a < 10; ``` -```+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| dot contents | -+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| -digraph HashRightJoin_7 { -subgraph cluster7{ +``` ++-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| dot contents | ++-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| +digraph Projection_8 { +subgraph cluster8{ node [style=filled, color=lightgrey] color=black label = "root" -"HashRightJoin_7" -> "TableReader_10" -"HashRightJoin_7" -> "TableReader_12" +"Projection_8" -> "HashJoin_9" +"HashJoin_9" -> "TableReader_13" +"HashJoin_9" -> "Selection_14" +"Selection_14" -> "TableReader_17" } -subgraph cluster9{ +subgraph cluster12{ node [style=filled, color=lightgrey] color=black label = "cop" -"Selection_9" -> "TableScan_8" +"Selection_12" -> "TableFullScan_11" } -subgraph cluster11{ +subgraph cluster16{ node [style=filled, color=lightgrey] color=black label = "cop" -"TableScan_11" +"Selection_16" -> "TableFullScan_15" } -"TableReader_10" -> "Selection_9" -"TableReader_12" -> "TableScan_11" +"TableReader_13" -> "Selection_12" +"TableReader_17" -> "Selection_16" } | -+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ ++-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) ``` @@ -215,6 +239,13 @@ The `xx.dot` is the result returned by the above statement. * TiDB 不像 MySQL 那样支持 `EXPLAIN FORMAT = JSON`。 * TiDB 目前不支持插入语句的 `EXPLAIN`。 +## `EXPLAIN FOR CONNECTION` + +`EXPLAIN FOR CONNECTION` 用于获得一个连接中最后执行的查询的执行计划,其输出格式与 `EXPLAIN` 完全一致。但 TiDB 中的实现与 MySQL 不同,除了输出格式之外,还有以下区别: + +- MySQL 返回的是**正在执行**的查询计划,而 TiDB 返回的是**最后执行**的查询计划。 +- MySQL 的文档中指出,MySQL 要求登录用户与被查询的连接相同,或者拥有 `PROCESS` 权限,而 TiDB 则要求登录用户与被查询的连接相同,或者拥有 `SUPER` 权限。 + ## 另请参阅 * [Understanding the Query Execution Plan](/reference/performance/understanding-the-query-execution-plan.md)