黄东旭解析 TiDB 的核心优势
535
2024-02-03
「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 taskCop task 是指使用 TiKV 中的 Coprocessor 执行的计算任务,root task 是指在 TiDB 中执行的计算任务。
SQL 优化的目标之一是将计算尽可能地下推到 TiKV 中执行TiKV 中的 Coprocessor 能支持大部分 SQL 内建函数(包括聚合函数和标量函数)、SQL LIMIT 操作、索引扫描和表扫描。
但是,所有的 Join 操作都只能作为 root task 在 TiDB 上执行利用 EXPLAIN 查看分析查询计划与其它主流商业数据库一样,TiDB 中可以通过 EXPLAIN 语句返回的结果查看某条 SQL 的执行计划。
EXPLAIN 语句目前 TiDB 的 EXPLAIN 主要输出 5 列,分别是:id,estRows,task,access object, operator info执行计划中每个算子都由这 5 列属性来描述,。
EXPLAIN结果中每一行描述一个算子每个属性的具体含义如下:EXPLAIN ANALYZE 语句和 EXPLAIN 不同,EXPLAIN ANALYZE 会执行对应的 SQL 语句,记录其运行时信息,和执行计划一并返回出来,可以视为 。
EXPLAIN 语句的扩展EXPLAIN ANALYZE 语句的返回结果中增加了 actRows, execution info, memory, disk 这几列信息:例如在下例中,优化器估算的 estRows
和实际执行中统计得到的 actRows 几乎是相等的,说明优化器估算的行数与实际行数的误差很小同时 IndexLookUp_10 算子在实际执行过程中使用了约 9 KB 的内存,该 SQL 在执行过程中,没有触发过任何算子的落盘操作。
mysql>explainanalyzeselect*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,prockeys: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 |+-------------------------------+---------+---------+-----------+-------------------------+------------------------------------------------------------------------+-----------------------------------------------------+---------------+------+
3rowsinset(0.00 sec)Copy查看计划中算子的执行顺序TiDB 的执行计划是一个树形结构,树中每个节点即是算子考虑到每个算子内多线程并发执行的情况,在一条 SQL 执行的过程中,如果能够有一个手术刀把这棵树切开看看,大家可能会发现所有的算子都正在消耗 CPU 和内存处理数据,从这个角度来看,算子是没有执行顺序的。
但是如果从一行数据先后被哪些算子处理的角度来看,一条数据在算子上的执行是有顺序的这个顺序可以通过下面这个规则简单总结出来:Build总是先于 Probe 执行,并且 Build 总是出现 Probe 前面
这个原则的前半句是说:如果一个算子有多个子节点,子节点 ID 后面有 Build 关键字的算子总是先于有 Probe 关键字的算子执行后半句是说:TiDB 在展现执行计划的时候,Build 端总是第一个出现,接着才是 。
Probe 端例如:TiDB(root@127.0.0.1:test)>explainselect*from t useindex(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 |+-------------------------------+---------+-----------+-------------------------+---------------------------------------------+
3rowsinset(0.00 sec)Copy这里 IndexLookUp_7 算子有两个孩子节点:IndexRangeScan_5(Build) 和 TableRowIDScan_6(Probe)可以看到,。
IndexRangeScan_5(Build) 是第一个出现的,并且基于上面这条规则,要得到一条数据,需要先执行它得到一个 RowID 以后,再由 TableRowIDScan_6(Probe) 根据前者读上来的
RowID 去获取完整的一行数据这种规则隐含的另一个信息是:在同一层级的节点中,出现在最前面的算子可能是最先被执行的,而出现在最末尾的算子可能是最后被执行的例如下面这个例子:TiDB(root@127.0.0.1。
:test)>explainselect*from t t1 useindex(idx_a)join t t2 useindex()where t1.a = t2.a;+----------------------------------+----------+-----------+--------------------------+------------------------------------------------------------------+
| id | estRows | task | access object | operator info
|+----------------------------------+----------+-----------+--------------------------+------------------------------------------------------------------+
| HashJoin_22 |12487.50| root ||innerjoin,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
|+----------------------------------+----------+-----------+--------------------------+------------------------------------------------------------------+
7rowsinset(0.00 sec)Copy要完成 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 端是 IndexFullScan 或 IndexRangeScan 类型的算子,Probe 端是 TableRowIDScan 类型的算子IndexMerge:和 。
IndexLookupReader 类似,可以看做是它的扩展,可以同时读取多个索引的数据,有多个 Build 端,一个 Probe 端执行过程也很类似,先汇总所有 Build 端 TiKV 扫描上来的 RowID,再去 Probe 端上根据这些 RowID 精确地读取 TiKV 上的数据。
Build 端是 IndexFullScan 或 IndexRangeScan 类型的算子,Probe 端是 TableRowIDScan 类型的算子IndexLookUp 示例:mysql>explain。
select*from t useindex(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 |+-------------------------------+----------+-----------+-------------------------+--------------------------------+
3rowsinset(0.00 sec)Copy这里 IndexLookUp_6 算子有两个孩子节点:IndexFullScan_4(Build) 和 TableRowIDScan_5(Probe)可以看到,。
IndexFullScan_4(Build) 执行索引全表扫,扫描索引 a 的所有数据,因为是全范围扫,这个操作将获得表中所有数据的 RowID,之后再由 TableRowIDScan_5(Probe)
去根据这些 RowID 去扫描所有的表数据可以预见的是,这个执行计划不如直接使用 TableReader 进行全表扫,因为同样都是全表扫,这里的 IndexLookUp 多扫了一次索引,带来了额外的开销。
TableReader 示例:mysql>explainselect*from t where a >1or 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 |+-------------------------+----------+-----------+---------------+----------------------------------------+
3rowsinset(0.00 sec)Copy在上面例子中 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>explainselect*from t useindex(idx_a, idx_b)where a >1or 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 |
+------------------------------+---------+-----------+-------------------------+------------------------------------------------+
4rowsinset(0.00 sec)CopyIndexMerge 使得数据库在扫描表数据时可以使用多个索引这里 IndexMerge_16 算子有三个孩子节点,其中 IndexRangeScan_13。
和 IndexRangeScan_14 根据范围扫描得到符合条件的所有 RowID,再由 TableRowIDScan_15 算子根据这些 RowID 精确的读取所有满足条件的数据查看聚合计算的执行计划。
Hash Aggregate 示例:TiDB 上的 Hash Aggregation 算子采用多线程并发优化,执行速度快,但会消耗较多内存下面是一个 Hash Aggregate 的例子:TiDB(root
@127.0.0.1:test)>explainselect/*+ 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 |+---------------------------+----------+-----------+---------------+---------------------------------+
4rowsinset(0.00 sec)Copy一般而言 TiDB 的 Hash Aggregate 会分成两个阶段执行,一个在 TiKV/TiFlash 的 Coprocessor 上,计算聚合函数的中间结果。
另一个在 TiDB 层,汇总所有 Coprocessor Task 的中间结果后,得到最终结果Stream Aggregate 示例:TiDB Stream Aggregation 算子通常会比 Hash Aggregate。
占用更少的内存,有些场景中也会比 Hash Aggregate 执行的更快当数据量太大或者系统内存不足时,可以试试 Stream Aggregate 算子一个 Stream Aggregate 的例子如下:。
TiDB(root@127.0.0.1:test)>explainselect/*+ 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 |+----------------------------+----------+-----------+---------------+---------------------------------+
4rowsinset(0.00 sec)Copy和 Hash Aggregate 类似,一般而言 TiDB 的 Stream Aggregate 也会分成两个阶段执行,一个在 TiKV/TiFlash 的
Coprocessor 上,计算聚合函数的中间结果另一个在 TiDB 层,汇总所有 Coprocessor Task 的中间结果后,得到最终结果查看 Join 的执行计划TiDB 的 Join 算法包括如下几类:。
Hash JoinMerge JoinIndex Hash JoinIndex Merge JoinApply下面分别通过一些例子来解释这些 Join 算法的执行过程Hash Join 示例:TiDB 的 Hash Join 算子采用了多线程优化,执行速度较快,但会消耗较多内存。
一个 Hash Join 的例子如下:mysql>explainselect/*+ 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 ||innerjoin,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
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。