接一篇TiDB执行计划(一),上一篇中主要介绍了执行计划中涉及到的算子,今天把执行计划中剩余的东西讲完
查询计划命令
EXPLAIN命令,可以查看TiDB执行sql时的执行计划,用法和mysql一样,跟上sql即可
EXPLAIN SQL语句
举个栗子(脱敏数据)
执行 EXPLAIN
EXPLAIN
select
a0_.id,
a0_.create_time,
a0_.end_time,
a0_.flow_id,
a0_.campaign_id,
a0_.unit_id,
a0_.oa_id,
a0_.org_path_,
a0_.param,
a0_.start_time,
a0_.state,
a0_.user_type,
a0_.update_time,
a0_.user_id
from
table_a a0_
where
a0_.campaign_id = 354361236223
and a0_.user_id = 25325123
and a0_.user_type = 1
and a0_.param = '1'
limit
1000
执行计划结果

执行计划结果
执行计划以一个树形结构展示出来,来说说每一列的含义吧:
-
id为算子,是执行sql时,每一步需要执行子任务 -
estRows为每一个子任务预估需要处理的行数 -
task为子任务执行时候所在的位置 -
access-object子任务的对象,比如说表、索引等 -
operator info子任务执行时候的一些算是操作日志的信息吧
上一篇文章说了算子,今天来说下执行计划中,剩下这几个字段estRows、task、access-object、operator info的含义吧
estRows:为每一个子任务预估需要处理的行数
这个很容易理解,就直接上栗子了
select
user_id
from
tablea a0_
GROUP by
user_id
这个sql,对于索引列user_id使用了group by,导致了执行时需要对所有索引数据进行扫描,会出现IndexFullScan算子,执行计划如下:

estRows
- 因为这个sql的执行计划是,先对于索引列
user_id进行了索引数据全量进行扫描,使用了IndexFullScan算子,所以IndexFullScan_11这一步的算子的预估行数estRows是索引列user_id全量数据的数据量,133270314 - 这个sql后一步的执行是通过
IndexReader算子对下层算子的数据进行一个聚合,所以IndexReader_13算子的预估行数estRows是user_id列 group by以后的数据,也就是873229.35了
task:为子任务执行时候所在的位置
- 为子任务执行时候所在的位置
- 主要有两种
-
cop,是指使用TiKV中的Coprocessor执行的计算任务,支持大部分函数(包括聚合函数和标量函数)、LIMIT操作、索引扫描和表扫描 -
root,是指在TiDB中执行的计算任务,一般所有汇聚TiKV/TiFlash上扫描的数据或者计算结果的算子都只能作为roottask在TiDB上执行,所有的Join操作都只能作为roottask在TiDB上执行 - TiDB的SQL优化的目标之一是将计算尽可能地下推到
TiKV中执行
举个栗子
栗子1:聚合查询栗子,使用COUNT:
select
COUNT(user_id)
from
tablea a0_
这个sql,对于索引列user_id使用了COUNT函数,导致了执行时需要对所有索引数据进行扫描,会出现IndexFullScan算子,执行计划如下:

COUNT函数
- 对于索引列
user_id使用了COUNT函数,先会对索引列user_id进行索引数据全量扫描,IndexFullScan_19算子的执行位置为cop[tikv] - 后续执行
SteamAgg_8算子时候,因为是 聚合函数,也会在cop[tikv]上执行 - 最终的
IndexReader_21算子对下层算子的数据进行一个聚合,在root也就是TiDB中执行
栗子2:聚合查询栗子,使用group by:
select
user_id
from
tablea a0_
GROUP by
user_id
这个sql,对于索引列user_id使用了group by,导致了执行时需要对所有索引数据进行扫描,会出现IndexFullScan算子,执行计划如下:

group by
- 对于索引列
user_id使用了group by,先会对索引列user_id进行索引数据全量扫描,IndexFullScan_11算子的执行位置为cop[tikv] - 后续执行
HashAgg_5算子时候,因为是group by,也会在cop[tikv]上执行 - 最终的
IndexReader_13算子对下层算子的数据进行一个聚合,在root也就是TiDB中执行
栗子3:子查询栗子,使用索引IN 子查询,当子查询为全量时:
select
*
from
tablea a0_
where
user_id IN (
select
user_id
from
tablea
)
这个sql,对于索引列user_id使用了in,子查询为全表扫描,所以会导致外层查询会对索引列user_id进行全索引数据进行扫描,会出现IndexFullScan算子,执行计划如下:

IN 子查询
- 首先,子查询没有加条件,是一个全表扫描,看执行计划2的地方,出现了一个
TableFullScan_49,由于子查询是全量数据,会在cop[tikv]上执行 - 聚合子查询结果的
TableReader_50会在root也就是TiDB中执行 - 看执行计划1的地方,当外层sql对索引列
user_id进行In时候,会对索引列user_id进行全索引数据的扫描,IndexFullScan_40会在cop[tikv]上执行 - 同样1位置的聚合算子
IndexReader_42在root也就是TiDB中执行 - 最终的
HashJoin_22算子对下层算子的数据进行一个聚合,在root也就是TiDB中执行
access-object: 子任务的对象,比如说表、索引等
这个很容易理解,就直接上栗子了
select
*
from
tablea a1_
where
a1_.user_id = 123214125
执行计划如下:

TableRowIDScan栗子
- 看这个sql,是一个通过索引列
user_id进行了索引范围扫描,他的执行逻辑是,先通过对于索引列user_id进行了一个范围扫描,得到所有符合条件的rowId,然后通过rowId扫描表获得数据,看执行也是,首先在Build端,通过IndexRangeScan算子,对于索引列user_id进行了范围扫描,扫描到的rowId,在Probe端,在通过TableRowIDScan算子,通过rowId扫描表获取数据,最终通过IndexLookUp算子来汇聚最终的数据 - 看这个sql执行计划的
access-object - 首先在
Build端,通过IndexRangeScan_8(Build)算子,对于索引列user_id进行了范围扫描,所以该算子的对象是table:a0_,index:idx_user_id(user_id),意思为操作的对象是表a0_的索引idx_user_id(user_id) - 然后通过范围扫描索引得到的
rowId扫描表获得数据,所以TableRowIDScan_9(Probe)算子的操作对象是表a0_
operator info: 子任务执行时候的一些算是操作日志的信息
这个很容易理解,基本是每一步的操作日志,就不举栗子说明,从原来的栗子中都可以看的懂
TiDB执行计划中的算子就为大家说到这里,欢迎大家来交流,指出文中一些说错的地方,让我加深认识。