最近排查了项目中TiDB慢sql,查询执行计划时,发现TiDB执行计划详情和mysql的还有一些区别,今天来学习分享一下,发现内容有点过长,分几部分吧,今天主要来说说执行计划中算子吧
查询计划命令
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
子任务执行时候的一些算是操作日志的信息吧
今天主要讲讲算子
id:为算子,是执行sql时,每一步需要执行子任务
算子是为返回查询结果而执行的特定步骤
TiDB的算子主要分成为两类,第一类为扫表类操作的算子,第二类为汇聚TiKV/TiFlash上扫描的数据或者计算结果的算子
第一类算子:扫表类操作的算子
扫表类操作的算子有如下几类:
TableFullScan:全表扫描
一般查询条件中没有用到索引或者索引失效了,执行计划中就会出现TableFullScan
TableFullScan栗子
:
select
*
from
tablea a0_
这个sql,没有用到索引肯定就全表扫描了,执行计划如下:
看的到执行计划中,出现TableFullScan
,id为TableFullScan
+ 了一个序号,说明,这一步执行的子任务进行全表扫描
IndexFullScan:全量扫描索引数据
IndexFullScan栗子1:聚合查询IndexFullScan栗子,使用COUNT
:
select
COUNT(user_id)
from
tablea a0_
这个sql,对于索引列user_id
使用了COUNT函数
,导致了执行时需要对所有索引数据进行扫描,会出现IndexFullScan
算子,执行计划如下:
看的到执行计划中,出现IndexFullScan
,id为IndexFullScan
+ 了一个序号,说明,这一步执行的子任务进行对索引列user_id
进行了全索引数据的扫描
IndexFullScan栗子2:聚合查询IndexFullScan栗子,使用group by
:
select
user_id
from
tablea a0_
GROUP by
user_id
这个sql,对于索引列user_id
使用了group by
,导致了执行时需要对所有索引数据进行扫描,会出现IndexFullScan
算子,执行计划如下:
看的到执行计划中,出现IndexFullScan
,id为IndexFullScan
+ 了一个序号,说明,这一步执行的子任务进行对索引列user_id
进行了全索引数据的扫描
IndexFullScan栗子3:聚合查询IndexFullScan栗子,使用min函数
:
select
MIN(user_id)
from
tablea a0_
这个sql,对于索引列user_id
使用了min函数
,导致了执行时需要对所有索引数据进行扫描,会出现IndexFullScan
算子,执行计划如下:
看的到执行计划中,出现IndexFullScan
,id为IndexFullScan
+ 了一个序号,说明,这一步执行的子任务进行对索引列user_id
进行了全索引数据的扫描
IndexFullScan栗子4:子查询IndexFullScan栗子,使用索引IN 子查询,当子查询为全量时
:
select
*
from
tablea a0_
where
user_id IN (
select
user_id
from
tablea
)
这个sql,对于索引列user_id
使用了in,子查询为全表扫描,所以会导致外层查询会对索引列user_id
进行全索引数据进行扫描,会出现IndexFullScan
算子,执行计划如下:
来看看执行计划,首先,子查询没有加条件,是一个全表扫描,看执行计划2的地方,出现了一个TableFullScan_49
,由于子查询是全量数据,所以当外层sql对索引列user_id
进行In时候,会对索引列user_id
进行全索引数据的扫描,出现IndexFullScan
IndexFullScan栗子5:join查询IndexFullScan栗子,使用left join,当左联表为全量数据时
:
select
a0_.*,
a1_.*
from
tablea a0_
LEFT JOIN (
select
*
from
tablea
) as a1_ ON a0_.user_id = a1_.user_id
这个sql,使用了索引列user_id
进行了left join
,当左联表为全表扫描时,会导致对索引列user_id
进行全索引数据进行扫描,会出现IndexFullScan
算子,执行计划如下:
来看看执行计划,左联表是一个全表扫描,所以会对索引列user_id
进行全索引数据的扫描,出现IndexFullScan
TableRowIDScan:根据上层传递下来的rowId扫描表数据,通俗的讲,就是查询先走索引获取到rowId,在根据rowId读取数据
根据上层传递下来的rowId
扫描表数据,通俗的讲,就是查询先走索引获取到rowId
,在根据rowId
读取数据,执行计划中就会出现TableRowIDScan
,举个栗子
TableRowIDScan栗子
:
select
*
from
tablea a1_
where
a1_.user_id = 123214125
就一个简单的sql来看一下,执行计划如下:
因为使用了索引列user_id
,所以,查询方式是从索引获取到了rowId
,通过rowId
去读取表数据,所以看到执行计划中,出现TableRowIDScan
,id为TableRowIDScan
+ 了一个序号,说明,这一步执行的子任务是通过送索引获取到的rowId
扫描表数据
IndexRangeScan:带有范围的索引数据扫描
带有范围的索引数据扫描,还是用这个栗子吧
TableRowIDScan栗子
:
select
*
from
tablea a1_
where
a1_.user_id = 123214125
就一个简单的sql来看一下,执行计划如下:
因为对索引列user_id
使用范围查询,所以看到执行计划中,出现IndexRangeScan
,id为IndexRangeScan
+ 了一个序号,说明,这一步执行的子任务是带有范围的索引数据扫描
第二类算子:汇聚TiKV/TiFlash上扫描的数据或者计算结果的算子
数据汇聚类的算子有如下几类:
TableReader:将上底层扫表算子TableFullScan或TableRangeScan得到的数据进行汇总
将上底层扫表算子TableFullScan
或TableRangeScan
得到的数据进行汇总
TableReader汇聚全表扫描TableFullScan的栗子
:
select
*
from
tablea a1_
这个sql,没有用到索引肯定就全表扫描了,执行计划如下:
看的到执行计划中,因为没有使用索引查询,进行了全表扫描,出现了TableFullScan
,所以最终使用了TableReader
算子,对于全表扫描的数据进行了汇总
IndexReader:将上底层扫表算子IndexFullScan或IndexRangeScan得到的数据进行汇总
将上底层扫表算子IndexFullScan
或IndexRangeScan
得到的数据进行汇总
IndexReader汇聚全量索引扫描IndexFullScan的栗子
:
select
MIN(user_id)
from
tablea a0_
还是使用这个sql,由于对索引列使用min函数,所以会对对全量索引进行扫描,出现了IndexFullScan
算子,所以会有IndexReader
算子对于IndexFullScan
算子得到数据进行汇总,执行计划如下:
IndexLookUp
先汇总Build端TiKV扫
描上来的RowID
,再去Probe端
上根据这些RowID
精确地读取TiKV
上的数据。Build 端是 IndexFullScan
或 IndexRangeScan
类型的算子,Probe端
是 TableRowIDScan
类型的算子,用sql举栗子吧
IndexLookUp栗子
:
select
*
from
tablea a1_
where
a1_.user_id = 123214125
执行计划如下:
看这个sql,是一个通过索引列user_id
进行了索引范围扫描,和上面讲的一样,他的执行逻辑是,先通过对于索引列user_id
进行了一个范围扫描,得到所有符合条件的rowId
,然后通过rowId
扫描表获得数据,看执行也是,首先在Build
端,通过IndexRangeScan
算子,对于索引列user_id
进行了范围扫描,扫描到的rowId
,在Probe
端,在通过TableRowIDScan
算子,通过rowId
扫描表获取数据,最终通过IndexLookUp
算子来汇聚最终的数据
3.算子执行的顺序
- 算子的结构是树状的,但在查询执行过程中,并不严格要求子节点任务在父节点之前完成。而且TiDB执行同一查询的各个节点并行执行
- 还是以上个sql为栗子
select
a0_.*,
a1_.*
from
tablea a0_
LEFT JOIN (
select
*
from
tablea
) as a1_ ON a0_.user_id = a1_.user_id
执行计划如下:
每一层级上,Build端
总是先于Probe端
执行,并且Build端
总是出现在Probe端
前面
TiDB执行计划中的算子就为大家说到这里,后面会为大家补上task等的信息,欢迎大家来交流,指出文中一些说错的地方,让我加深认识。