使用 EXPLAIN 解读执行计划

    以 中的一个 SQL 语句为例,该语句统计了 2017 年 7 月 1 日的行程次数:

    1. +------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+
    2. | id | estRows | task | access object | operator info |
    3. +------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+
    4. | StreamAgg_20 | 1.00 | root | | funcs:count(Column#13)->Column#11 |
    5. | └─TableReader_21 | 1.00 | root | | data:StreamAgg_9 |
    6. | └─StreamAgg_9 | 1.00 | cop[tikv] | | funcs:count(1)->Column#13 |
    7. | └─Selection_19 | 250.00 | 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) |
    8. | └─TableFullScan_18 | 10000.00 | cop[tikv] | table:trips | keep order:false, stats:pseudo |
    9. +------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+
    10. 5 rows in set (0.00 sec)

    以上是该查询的执行计划结果。从 └─TableFullScan_18 算子开始向上看,查询的执行过程如下(非最佳执行计划):

    1. Coprocessor (TiKV) 读取整张 trips 表的数据,作为一次 TableFullScan 操作,再将读取到的数据传递给 Selection_19 算子。Selection_19 算子仍在 TiKV 内。

    2. Selection_19 算子根据谓词 WHERE start_date BETWEEN .. 进行数据过滤。预计大约有 250 行数据满足该过滤条件(基于统计信息以及算子的执行逻辑估算而来)。└─TableFullScan_18 算子显示 stats:pseudo,表示该表没有实际统计信息,执行 ANALYZE TABLE trips 收集统计信息后,预计的估算的数字会更加准确。

    3. COUNT 函数随后应用于满足过滤条件的行,这一过程也是在 TiKV (cop[tikv]) 中的 StreamAgg_9 算子内完成的。TiKV coprocessor 能执行一些 MySQL 内置函数,COUNT 是其中之一。

    4. StreamAgg_9 算子执行的结果会被传递给 TableReader_21 算子(位于 TiDB 进程中,即 root 任务)。执行计划中,TableReader_21 算子的 estRows1,表示该算子将从每个访问的 TiKV Region 接收一行数据。这一请求过程的详情,可参阅 EXPLAIN ANALYZE

    5. StreamAgg_20 算子随后对 └─TableReader_21 算子传来的每行数据计算 COUNT 函数的结果。StreamAgg_20 是根算子,会将结果返回给客户端。

    EXPLAIN 语句只返回查询的执行计划,并不执行该查询。若要获取实际的执行时间,可执行该查询,或使用 语句:

    1. EXPLAIN ANALYZE SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59';
    1. +------------------------------+----------+----------+-----------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+-----------+------+
    2. | id | estRows | actRows | task | access object | execution info | operator info | memory | disk |
    3. +------------------------------+----------+----------+-----------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+-----------+------+
    4. | StreamAgg_20 | 1.00 | 1 | root | | time:1.031417203s, loops:2 | funcs:count(Column#13)->Column#11 | 632 Bytes | N/A |
    5. | └─StreamAgg_9 | 1.00 | 56 | cop[tikv] | | proc max:640ms, min:8ms, p80:276ms, p95:480ms, iters:18695, tasks:56 | funcs:count(1)->Column#13 | N/A | N/A |
    6. | └─Selection_19 | 250.00 | 11409 | cop[tikv] | | proc max:640ms, min:8ms, p80:276ms, p95:476ms, iters:18695, tasks:56 | ge(bikeshare.trips.start_date, 2017-07-01 00:00:00.000000), le(bikeshare.trips.start_date, 2017-07-01 23:59:59.000000) | N/A | N/A |
    7. | └─TableFullScan_18 | 10000.00 | 19117643 | cop[tikv] | table:trips | proc max:612ms, min:8ms, p80:248ms, p95:460ms, iters:18695, tasks:56 | keep order:false, stats:pseudo | N/A | N/A |
    8. +------------------------------+----------+----------+-----------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+-----------+------+
    9. 5 rows in set (1.03 sec)

    执行以上示例查询耗时 1.03 秒,说明执行性能较为理想。

    以上 EXPLAIN ANALYZE 的结果中,actRows 表明一些 estRows 预估数不准确(预估返回 10000 行数据但实际返回 19117643 行)。└─TableFullScan_18 算子的 operator info 列 (stats:pseudo) 信息也表明该算子的预估数不准确。

    如果先执行 再执行 EXPLAIN ANALYZE,预估数与实际数会更接近:

    1. ANALYZE TABLE trips;
    2. EXPLAIN ANALYZE SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59';

    执行 ANALYZE TABLE 后,可以看到 └─TableFullScan_18 算子的预估行数是准确的,└─Selection_19 算子的预估行数也更接近实际行数。以上两个示例中的执行计划(即 TiDB 执行查询所使用的一组算子)未改变,但过时的统计信息常常导致 TiDB 选择到非最优的执行计划。

    ANALYZE TABLE 外,达到 tidb_auto_analyze_ratio 阈值后,TiDB 会自动在后台重新生成统计数据。若要查看 TiDB 有多接近该阈值(即 TiDB 判断统计数据有多健康),可执行 语句。

    1. SHOW STATS_HEALTHY;
    1. +-----------+------------+----------------+---------+
    2. | Db_name | Table_name | Partition_name | Healthy |
    3. +-----------+------------+----------------+---------+
    4. | bikeshare | trips | | 100 |
    5. +-----------+------------+----------------+---------+
    6. 1 row in set (0.00 sec)

    确定优化方案

    当前执行计划是有效率的:

    • 大部分任务是在 TiKV 内处理的,需要通过网络传输给 TiDB 处理的仅有 56 行数据,每行都满足过滤条件,而且都很短。

    当前执行计划存在的最大问题在于谓词 start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59' 并未立即生效,先是 TableFullScan 算子读取所有行数据,然后才进行过滤选择。可以在 的返回结果中找出问题原因:

    1. SHOW CREATE TABLE trips\G
    1. *************************** 1. row ***************************
    2. Table: trips
    3. Create Table: CREATE TABLE `trips` (
    4. `trip_id` bigint(20) NOT NULL AUTO_INCREMENT,
    5. `duration` int(11) NOT NULL,
    6. `end_date` datetime DEFAULT NULL,
    7. `start_station_number` int(11) DEFAULT NULL,
    8. `start_station` varchar(255) DEFAULT NULL,
    9. `end_station_number` int(11) DEFAULT NULL,
    10. `end_station` varchar(255) DEFAULT NULL,
    11. `bike_number` varchar(255) DEFAULT NULL,
    12. `member_type` varchar(255) DEFAULT NULL,
    13. PRIMARY KEY (`trip_id`)
    14. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=20477318
    15. 1 row in set (0.00 sec)

    以上返回结果显示,start_date没有索引。要将该谓词下推到 index reader 算子,还需要一个索引。添加索引如下:

    1. Query OK, 0 rows affected (2 min 10.23 sec)

    添加索引后,可以使用 EXPLAIN 重复该查询。在以下返回结果中,可见 TiDB 选择了新的执行计划,而且不再使用 TableFullScanSelection 算子。

    1. EXPLAIN SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59';
    1. +-----------------------------+---------+-----------+-------------------------------------------+-------------------------------------------------------------------+
    2. | id | estRows | task | access object | operator info |
    3. +-----------------------------+---------+-----------+-------------------------------------------+-------------------------------------------------------------------+
    4. | StreamAgg_17 | 1.00 | root | | funcs:count(Column#13)->Column#11 |
    5. | └─IndexReader_18 | 1.00 | root | | index:StreamAgg_9 |
    6. | └─StreamAgg_9 | 1.00 | cop[tikv] | | funcs:count(1)->Column#13 |
    7. | └─IndexRangeScan_16 | 8471.88 | 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 |
    8. +-----------------------------+---------+-----------+-------------------------------------------+-------------------------------------------------------------------+
    9. 4 rows in set (0.00 sec)

    若要比较实际的执行时间,可再次使用 EXPLAIN ANALYZE 语句:

    1. EXPLAIN ANALYZE SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59';

    从以上结果可看出,查询时间已从 1.03 秒减少到 0.0 秒。