数据库系列之TiDB中的执行计划

TiDB中的SQL计算层TiDB Server会解析客户端的SQL请求并进行逻辑和物理上的优化,生成执行计划。本文简要介绍TiDB中的几种执行计划,了解不同算子在Explain中的输出信息。


1、TiDB SQL层架构

TiDB的SQL层即TiDB Server,负责将SQL翻译成Key-Value操作,将其转发给分布式Key-Value存储层TiKV,然后组装TiKV返回的结果,最终将查询结果返回给客户端。这一层的TiDB Server节点都是无状态的,节点本身并不存储数据,节点之间完全对等。

在这里插入图片描述

上图列出了TiDB SQL层重要的模块以及调用关系。用户的SQL请求会直接或者通过Load Balancer发送到TiDB Server,TiDB Server会解析MySQL Protocol Packet,获取请求内容,对SQL进行语法解析和语义分析,制定和优化查询计划,执行查询计划并获取和处理数据。数据全部存储在TiKV集群中,所以在这个过程中TiDB Server需要和TiKV交互,获取数据。最后TiDB Server需要将查询结果返回给用户。

1.1 表数据与key-value映射关系

TiDB中数据到(key,value)键值对的映射方案包括表数据和索引数据。

1)表数据和key-value映射关系

在关系型数据库中,一张表可能有多个列,而将一行多个列的表数据映射成key-value键值对,则需要考虑如何来构造key。比如在OLTP场景下,数据库需要快速读取某一行数据,对应的key最好有一个唯一ID方便快速定位;在OLAP场景下需要全表扫描,如果将一个表中的所有行的key编码放到一个区间内,就可以通过范围查询高效完成全表扫描任务。

  • 为了保证同一个表的数据放在一起, TiDB会为每个表分配一个表ID,用TableID表示。TableID是一个整数,在整个集群内唯一。
  • TiDB会为表中每行数据分配一个行ID,用RowID表示。RowID也是一个整数,在表内唯一。在TiDB中,对于RowID,如果某个表有整数型的主键,TiDB会使用主键的值当做这一行数据的行ID。

每行数据按照以上规则编码成(Key, Value)键值对:

Key: tablePrefix<<TableID>>_recordPrefixSep<<RowID>>  => Value: [col1, col2, col3, col4]

其中tablePrefix和recordPrefixSep是特定的字符串常量,用于在Key空间内区分其他数据

2)索引数据和key-value的映射关系

TiDB中支持主键索引和二级索引,在索引数据和key-value的映射关系中,TiDB会为表中的每个索引分配一个索引ID,用IndexID表示。

  • 对于主键和唯一索引,按照以下规则编码
Key: tablePrefix<<tableID>>_indexPrefixSep<<indexID>>_indexedColumnsValue => Value: RowID
  • 对于不需要满足唯一性约束的普通二级索引,一个键值可能对应多行,按照如下规则编码成(Key, Value)键值对:
Key: tablePrefix<<TableID>>_indexPrefixSep<<IndexID>>_indexedColumnsValue_<<RowID>> => Value: null

3)Key-value对应关系例子

假设TiDB中有以下表,表结构如下:

CREATE TABLE User {
    ID int,
    Name varchar(20),
    Age int,
    PRIMARY KEY (ID),
    KEY idxAge (Age)
};

表中有如下数据:

1, "TiDB", 10
2, "TiKV", 20
3, "PD",30

假设该表的TableID为101,则其存储在TiKV上的表数据为:

t101_r1 => ["TiDB",10]
t101_r2 => ["TiKV",20]
t101_r3 => ["PD",30]

该表还有一个非唯一的普通二级索引idxAge,假设这个索引的 IndexID 为1,则其存储在TiKV上的索引数据为:

t101_i1_10_1 => null
t101_i1_20_2 => null
t101_i1_30_3 => null
1.2 一条SQL的生命周期

SQL在TiDB中经过的流程如下所示:

  1. TiDB Server客户端发送的SQL请求在Parser中解析为TiDB能够理解的语法树AST
  2. TiDB中的SQL优化器首先会进行逻辑优化生成Logical Plan
  3. 再根据物理优化生成Physical,决定选择哪些索引和算子进行计算,过程中会使用到统计信息statistics的数据
  4. 生成的物理计划会在Executor中进行执行
    1. 如果是DML语句,TiDB会将用户更新的内容先缓存在Transaction模块中,等到用户执行事务的Commit时再进行两阶段提交,将结果写入到TiKV
    2. 对于复杂查询请求,TiDB 会通过DistSQL模块并行地向TiKV的多个region发送查询请求,然后再按照执行计划中的流程计算出查询结果来
  5. TiDB采用两阶段提交的事务模型。为此需要向PD请求一个全局逻辑时间戳TSO,用来表明事务的开始时间与提交时间。为了不给PD造成过多的请求压力,TiDB通过单个线程一次为多个事务分配时间

在这里插入图片描述

1.3 分布式SQL运算

在分布式数据库的场景下,计算应该需要尽量靠近存储节点,以避免大量的RPC调用。

  1. 首先,SQL中的谓词条件name = “TiDB” 被下推到每个存储节点进行计算,这样只需要返回有效的行,避免无意义的网络传输
  2. 然后,聚合函数Count()也可以被下推到存储节点,进行预聚合,每个节点只需要返回一个Count()的结果即可
  3. 再由SQL层将各个节点返回的Count(*)的结果累加求和。

以下是数据逐层返回的示意图:

在这里插入图片描述

以下是分布式执行引擎的例子:

在这里插入图片描述

2、TiDB执行计划

TiDB优化器会根据当前数据表的最新的统计信息来选择最优的执行计划,执行计划由一系列的算子构成,在TiDB中可通过EXPLAIN语句返回的结果查看某条SQL的执行计划。

2.1 EXPLAIN输出

TiDB的EXPLAIN会输出5列,分别是:id,estRows,task,access object, operator info。

mysql> explain select * from tab01;
+-----------------------+----------+-----------+---------------+--------------------------------+
| id                    | estRows  | task      | access object | operator info                  |
+-----------------------+----------+-----------+---------------+--------------------------------+
| TableReader_5         | 10000.00 | root      |               | data:TableFullScan_4           |
| └─TableFullScan_4     | 10000.00 | cop[tikv] | table:tab01   | keep order:false, stats:pseudo |
+-----------------------+----------+-----------+---------------+--------------------------------+

执行计划中每个算子都由这5列属性来描述,EXPLAIN结果中每一行描述一个算子。每个属性的具体含义如下:
在这里插入图片描述
通过观察EXPLAIN的结果,你可以知道如何给数据表添加索引使得执行计划使用索引从而加速SQL语句的执行速度;你也可以使用EXPLAIN来检查优化器是否选择了最优的顺序来JOIN数据表。

2.2 EXPLAIN ANALYZE输出

EXPLAIN ANALYZE会执行对应的SQL语句,记录其运行时信息,和执行计划一并返回出来,可以视为EXPLAIN语句的扩展。EXPLAIN ANALYZE语句的返回结果中增加了actRows, execution info,memory,disk这几列信息:
在这里插入图片描述

以下为例,优化器估算的estRows和实际执行中统计得到的actRows几乎是相等的,说明优化器估算的行数与实际行数的误差很小。同时TableReader_5算子在实际执行过程中使用了约282 Bytes 的内存,该 SQL 在执行过程中,没有触发过任何算子的落盘操作。

mysql> explain analyze select * from tab02;
+-----------------------+---------+---------+-----------+---------------+-----------------------------------------------------------------------+----------------------+-----------+------+
| id                    | estRows | actRows | task      | access object | execution info                                                        | operator info        | memory    | disk |
+-----------------------+---------+---------+-----------+---------------+-----------------------------------------------------------------------+----------------------+-----------+------+
| TableReader_5         | 4.00    | 4       | root      |               | time:1.626388ms, loops:2, rpc num: 1, rpc time:1.37835ms, proc keys:4 | data:TableFullScan_4 | 282 Bytes | N/A  |
| └─TableFullScan_4     | 4.00    | 4       | cop[tikv] | table:tab02   | time:0s, loops:1                                                      | keep order:false     | N/A       | N/A  |
+-----------------------+---------+---------+-----------+---------------+-----------------------------------------------------------------------+----------------------+-----------+------+
2.3 算子的执行顺序

TiDB的执行计划是一个树形结构,树中每个节点即是算子。考虑到每个算子内多线程并发执行的情况,在一条SQL执行的过程中,可能所有的算子都正在消耗CPU和内存处理数据,从这个角度来看,算子是没有执行顺序的。但是如果从一行数据先后被哪些算子处理的角度来看,一条数据在算子上的执行是有顺序的。这个顺序可以通过下面这个规则简单总结出来:
Build总是先于Probe执行,并且Build总是出现在Probe前面

  • 如果一个算子有多个孩子节点,孩子节点ID后面有Build关键字的算子总是先于有Probe关键字的算子执行
  • TiDB在展现执行计划的时候,Build端总是第一个出现,接着才是Probe端

以下为例:

mysql> create table tt04 (id int(11),age int(4),primary key(`id`),key idx1(`id`));
mysql> explain select * from tt04 use index(idx1) where age=1;
+------------------------------+----------+-----------+----------------------------+--------------------------------+
| id                           | estRows  | task      | access object              | operator info                  |
+------------------------------+----------+-----------+----------------------------+--------------------------------+
| IndexLookUp_8                | 10.00    | root      |                            |                                |
| ├─IndexFullScan_5(Build)     | 10000.00 | cop[tikv] | table:tt04, index:idx1(id) | keep order:false, stats:pseudo |
| └─Selection_7(Probe)         | 10.00    | cop[tikv] |                            | eq(tango.tt04.age, 1)          |
|   └─TableRowIDScan_6         | 10000.00 | cop[tikv] | table:tt04                 | keep order:false, stats:pseudo |
+------------------------------+----------+-----------+----------------------------+--------------------------------+

这里IndexLookUp_8算子有两个孩子节点:IndexFullScan_5(Build)和 Selection_7(Probe)。可以看到,IndexFullScan_5(Build)是第一个出现的,并且基于上面这条规则,要得到一条数据,需要先得到一个RowID以后,再由Selection_7(Probe)根据前者读上来的RowID去获取完整的一行数据。

2.4 扫表的执行计划

真正执行扫表操作的算子有如下几类:

在这里插入图片描述
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类型的算子。

1)IndexLookUp示例

mysql> create table tt01 (id int(11),age int(4),primary key(`id`),key idx1(`id`));
mysql> explain select * from tt01 use index(idx1);
+-------------------------------+----------+-----------+----------------------------+--------------------------------+
| id                            | estRows  | task      | access object              | operator info                  |
+-------------------------------+----------+-----------+----------------------------+--------------------------------+
| IndexLookUp_6                 | 10000.00 | root      |                            |                                |
| ├─IndexFullScan_4(Build)      | 10000.00 | cop[tikv] | table:tt01, index:idx1(id) | keep order:false, stats:pseudo |
| └─TableRowIDScan_5(Probe)     | 10000.00 | cop[tikv] | table:tt01                 | keep order:false, stats:pseudo |
+-------------------------------+----------+-----------+----------------------------+--------------------------------+
3 rows in set (0.07 sec)

这里IndexLookUp_6算子有两个孩子节点:IndexFullScan_4(Build)和TableRowIDScan_5(Probe)。

  • IndexFullScan_4(Build) 执行索引全表扫,扫描索引idx1的所有数据,因为是全范围扫,这个操作将获得表中所有数据的RowID
  • 由TableRowIDScan_5(Probe)根据这些RowID去扫描所有的表数据
  • 可以预见的是,这个执行计划不如直接使用TableReader进行全表扫,因为同样都是全表扫,这里的IndexLookUp多扫了一次索引,带来了额外的开销。

2)TableReader示例

mysql> create table tt02 (a int(11),b int(11),primary key(`a`),key idx1(`a`));
mysql> explain select * from tt02 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(tango.tt02.a, 1), gt(tango.tt02.b, 100)) |
|   └─TableFullScan_5     | 10000.00 | cop[tikv] | table:tt02    | keep order:false, stats:pseudo                 |
+-------------------------+----------+-----------+---------------+------------------------------------------------+
3 rows in set (0.11 sec)

在上面例子中TableReader_7算子的孩子节点是Selection_6,以这个孩子节点为根的子树被当做了一个Cop Task下发给了相应的TiKV,这个Cop Task使用TableFullScan_5算子执行扫表操作。由TableFullScan_5可以看到,这个执行计划使用了全表扫描的操作,集群的负载将因此而上升,可能会影响到集群中正在运行的其他查询。这时候如果能够建立合适的索引,并且使用IndexMerge算子,将能够极大的提升查询的性能,降低集群的负载。

3)IndexMerge示例
IndexMerge是TiDB v4.0中引入的一种对表的新访问方式。在这种访问方式下,TiDB优化器可以选择对一张表使用多个索引,并将每个索引的返回结果进行合并。在某些场景下,这种访问方式能够减少大量不必要的数据扫描,提升查询的执行效率。

mysql> create table tt03 (a int(11),b int(11),primary key(`a`),key idx_a(`a`),key idx_b(`b`));
Query OK, 0 rows affected (1.55 sec)

mysql> explain select * from tt03 where a=1 or b=1;
+-------------------------+----------+-----------+---------------+----------------------------------------------+
| id                      | estRows  | task      | access object | operator info                                |
+-------------------------+----------+-----------+---------------+----------------------------------------------+
| TableReader_7           | 8000.00  | root      |               | data:Selection_6                             |
| └─Selection_6           | 8000.00  | cop[tikv] |               | or(eq(tango.tt03.a, 1), eq(tango.tt03.b, 1)) |
|   └─TableFullScan_5     | 10000.00 | cop[tikv] | table:tt03    | keep order:false, stats:pseudo               |
+-------------------------+----------+-----------+---------------+----------------------------------------------+
3 rows in set (0.00 sec)

mysql> set @@tidb_enable_index_merge = 1;
Query OK, 0 rows affected (0.15 sec)

mysql> explain select * from tt03 use index(idx_a,idx_b) where a=1 or b=1;
+--------------------------------+---------+-----------+----------------------------+---------------------------------------------+
| id                             | estRows | task      | access object              | operator info                               |
+--------------------------------+---------+-----------+----------------------------+---------------------------------------------+
| IndexMerge_11                  | 20.00   | root      |                            |                                             |
| ├─IndexRangeScan_8(Build)      | 10.00   | cop[tikv] | table:tt03, index:idx_a(a) | range:[1,1], keep order:false, stats:pseudo |
| ├─IndexRangeScan_9(Build)      | 10.00   | cop[tikv] | table:tt03, index:idx_b(b) | range:[1,1], keep order:false, stats:pseudo |
| └─TableRowIDScan_10(Probe)     | 20.00   | cop[tikv] | table:tt03                 | keep order:false, stats:pseudo              |
+--------------------------------+---------+-----------+----------------------------+---------------------------------------------+
4 rows in set (0.04 sec)

在上述示例中,过滤条件是使用OR条件连接的WHERE子句。在启用 IndexMerge 前,每个表只能使用一个索引,不能将a = 1下推到索引idx_a,也不能将b = 1下推到索引idx_b。当 tt03中存在大量数据时,全表扫描的效率会很低。针对这类场景,TiDB引入了对表的新访问方式IndexMerge。

在IndexMerge访问方式下,优化器可以选择对一张表使用多个索引,并将每个索引的返回结果进行合并,生成一个 IndexMerge的执行计划。此时的IndexMerge_11算子有三个子节点,其中IndexRangeScan_8和 IndexRangeScan_9根据范围扫描得到符合条件的所有RowID,再由TableRowIDScan_10算子根据这些RowID精确地读取所有满足条件的数据。

2.5 聚合的执行计划

TiDB的聚合算法包括如下两类:

  • Hash Aggregate
  • Stream Aggregate

1)Hash Aggregate示例

TiDB上的Hash Aggregation算子采用多线程并发优化,执行速度快,但会消耗较多内存。下面是一个Hash Aggregate的例子:

mysql>  create table t3 (x int) partition by hash(x) partitions 2;
mysql> explain select /*+ HASH_AGG() */ count(*) from t3;
+------------------------------+----------+-----------+------------------------+--------------------------------+
| id                           | estRows  | task      | access object          | operator info                  |
+------------------------------+----------+-----------+------------------------+--------------------------------+
| HashAgg_10                   | 1.00     | root      |                        | funcs:count(1)->Column#3       |
| └─Union_13                   | 20000.00 | root      |                        |                                |
|   ├─TableReader_15           | 10000.00 | root      |                        | data:TableFullScan_14          |
|   │ └─TableFullScan_14       | 10000.00 | cop[tikv] | table:t3, partition:p0 | keep order:false, stats:pseudo |
|   └─TableReader_17           | 10000.00 | root      |                        | data:TableFullScan_16          |
|     └─TableFullScan_16       | 10000.00 | cop[tikv] | table:t3, partition:p1 | keep order:false, stats:pseudo |
+------------------------------+----------+-----------+------------------------+--------------------------------+
6 rows in set (0.01 sec)

一般而言TiDB的Hash Aggregate会分成两个阶段执行:

  • 一个在TiKV/TiFlash的Coprocessor上,在扫表算子读取数据时计算聚合函数的中间结果,如示例中的TableFullScan_14和TableFullScan_16
  • 另一个在TiDB层,汇总所有Coprocessor Task的中间结果后,得到最终结果,如示例中HashAgg_10
  • 其中explain表中的operator info列还记录了Hash Aggregation的其他信息,比如Aggregation所使用的聚合函数是什么。在上面的例子中,Hash Aggregation算子的operator info中的内容为funcs:count(1)->Column#3,我们可以得到Hash Aggregation使用了聚合函数count进行计算。

2)Stream Aggregate示例

TiDB中的Stream Aggregation算子通常会比Hash Aggregate占用更少的内存,有些场景中也会比Hash Aggregate执行得更快。当数据量太大或者系统内存不足时,可以试试Stream Aggregate算子,如下例所示:

mysql> explain select /*+ STREAM_AGG() */ count(*) from tt01;
+----------------------------+----------+-----------+---------------+---------------------------------+
| id                         | estRows  | task      | access object | operator info                   |
+----------------------------+----------+-----------+---------------+---------------------------------+
| StreamAgg_20               | 1.00     | root      |               | funcs:count(Column#5)->Column#3 |
| └─TableReader_21           | 1.00     | root      |               | data:StreamAgg_8                |
|   └─StreamAgg_8            | 1.00     | cop[tikv] |               | funcs:count(1)->Column#5        |
|     └─TableFullScan_18     | 10000.00 | cop[tikv] | table:tt01    | keep order:false, stats:pseudo  |
+----------------------------+----------+-----------+---------------+---------------------------------+
4 rows in set (0.01 sec)

和Hash Aggregate类似,一般而言TiDB的Stream Aggregate也会分成两个阶段执行,一个在TiKV/TiFlash的Coprocessor上,在扫表算子读取数据时计算聚合函数的中间结果,比如上例中的TableFullScan_18。另一个在TiDB层,汇总所有Coprocessor Task的中间结果后,得到最终结果,如上例中的StreamAgg_20。

2.6 Join的执行计划

TiDB 的Join算法包括如下几类:

  • 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)

1)Hash Join示例

TiDB的Hash Join算子采用了多线程优化,执行速度较快,但会消耗较多内存。如下所示:

mysql> create table tt04 (id int(11),a int(4));
mysql> create table tt05 (id int(11),a int(4));
mysql> EXPLAIN SELECT /*+ HASH_JOIN(tt04, tt05) */ * FROM tt04, tt05 WHERE tt04.a = tt05.a;
+------------------------------+----------+-----------+---------------+----------------------------------------------------+
| id                           | estRows  | task      | access object | operator info                                      |
+------------------------------+----------+-----------+---------------+----------------------------------------------------+
| HashJoin_8                   | 12487.50 | root      |               | inner join, equal:[eq(tango.tt04.a, tango.tt05.a)] |
| ├─TableReader_15(Build)      | 9990.00  | root      |               | data:Selection_14                                  |
| │ └─Selection_14             | 9990.00  | cop[tikv] |               | not(isnull(tango.tt05.a))                          |
| │   └─TableFullScan_13       | 10000.00 | cop[tikv] | table:tt05    | keep order:false, stats:pseudo                     |
| └─TableReader_12(Probe)      | 9990.00  | root      |               | data:Selection_11                                  |
|   └─Selection_11             | 9990.00  | cop[tikv] |               | not(isnull(tango.tt04.a))                          |
|     └─TableFullScan_10       | 10000.00 | cop[tikv] | table:tt04    | keep order:false, stats:pseudo                     |
+------------------------------+----------+-----------+---------------+----------------------------------------------------+
7 rows in set (0.00 sec)

Hash Join会将Build端的数据缓存在内存中,根据这些数据构造出一个Hash Table,然后读取Probe端的数据,用Probe端的数据去探测Build端构造出来的Hash Table,将符合条件的数据返回给用户。其中explain表中的operator info列还记录了Hash Join的其他信息,包括Join的类型、Join的条件是什么。如上例中,该查询是一个Inner Join,其中Join的条件 equal:[eq(tango.tt04.a, tango.tt05.a)],和查询语句中 where tt04.a = tt05.a部分对应。

2)Merge Join示例

TiDB的Merge Join算子相比于Hash Join通常会占用更少的内存,但可能执行时间会更久。当数据量太大,或系统内存不足时,建议尝试使用。如下例所示:

mysql> create table tt04 (id int(11),a int(11),primary key(`id`),key idx1(`a`));
mysql> create table tt05 (id int(11),a int(11),primary key(`id`),key idx1(`a`));
Query OK, 0 rows affected (1.03 sec)

mysql> EXPLAIN SELECT /*+ MERGE_JOIN(tt05, tt04) */ * FROM tt05, tt04 WHERE tt05.a = tt04.a;
+-----------------------------+----------+-----------+---------------------------+-----------------------------------------------------------+
| id                          | estRows  | task      | access object             | operator info                                             |
+-----------------------------+----------+-----------+---------------------------+-----------------------------------------------------------+
| MergeJoin_8                 | 12487.50 | root      |                           | inner join, left key:tango.tt05.a, right key:tango.tt04.a |
| ├─IndexReader_36(Build)     | 9990.00  | root      |                           | index:IndexFullScan_35                                    |
| │ └─IndexFullScan_35        | 9990.00  | cop[tikv] | table:tt04, index:idx1(a) | keep order:true, stats:pseudo                             |
| └─IndexReader_34(Probe)     | 9990.00  | root      |                           | index:IndexFullScan_33                                    |
|   └─IndexFullScan_33        | 9990.00  | cop[tikv] | table:tt05, index:idx1(a) | keep order:true, stats:pseudo                             |
+-----------------------------+----------+-----------+---------------------------+-----------------------------------------------------------+
5 rows in set (0.00 sec)

Merge Join算子在执行时,会从Build端把一个Join Group的数据全部读取到内存中(示例中的IndexFullScan_35),接着再去读Probe端的数据(IndexFullScan_33),用Probe端的每行数据去和Build端的一个完整Join Group比较,依次查看是否匹配(示例中的MergeJoin_8)。Join Group指的是所有Join Key上值相同的数据。

3)Index Merge Join示例

该算法的使用条件包含Index Join的所有使用条件,但还需要添加一条:join keys 中的内表列集合是内表使用的 index 的前缀,或内表使用的index是join keys中的内表列集合的前缀,该算法相比于INL_JOIN会更节省内存。

mysql> create table tt02 (a int(11),b int(11),primary key(`a`),key idx1(`a`));
mysql> create table tt03 (a int(11),b int(11),primary key(`a`),key idx1(`a`));
mysql> insert into tt03 values(1,1)
mysql> EXPLAIN SELECT /*+ INL_MERGE_JOIN(tt02, tt03) */ * FROM tt02, tt03 WHERE tt02.a = tt03.a;
+-------------------------------+---------+-----------+---------------+----------------------------------------------------------------------------------+
| id                            | estRows | task      | access object | operator info                                                                    |
+-------------------------------+---------+-----------+---------------+----------------------------------------------------------------------------------+
| Projection_7                  | 2.50    | root      |               | tango.tt02.a, tango.tt02.b, tango.tt03.a, tango.tt03.b                           |
| └─IndexMergeJoin_16           | 2.50    | root      |               | inner join, inner:TableReader_14, outer key:tango.tt03.a, inner key:tango.tt02.a |
|   ├─TableReader_41(Build)     | 2.00    | root      |               | data:TableFullScan_40                                                            |
|   │ └─TableFullScan_40        | 2.00    | cop[tikv] | table:tt03    | keep order:false                                                                 |
|   └─TableReader_14(Probe)     | 1.00    | root      |               | data:TableRangeScan_13                                                           |
|     └─TableRangeScan_13       | 1.00    | cop[tikv] | table:tt02    | range: decided by [tango.tt03.a], keep order:true, stats:pseudo                  |
+-------------------------------+---------+-----------+---------------+----------------------------------------------------------------------------------+
6 rows in set (0.01 sec)
2.7 优化实例

1)下载数据bikeshare example database

[root@tango-01 src]#  mkdir -p bikeshare-data && cd bikeshare-data
[root@tango-01 src]# curl -L --remote-name-all https://s3.amazonaws.com/capitalbikeshare-data/{2017..2017}-capitalbikeshare-tripdata.zip
[root@tango-01 src]# unzip \*-tripdata.zip
[root@tango-01 bikeshare-data]# ll
total 595752
-rw-r--r-- 1 root root  89576218 Mar 14 09:44 2017-capitalbikeshare-tripdata.zip
-rw-r--r-- 1 root root  89276248 Mar 15  2018 2017Q1-capitalbikeshare-tripdata.csv
-rw-r--r-- 1 root root 153392619 Mar 15  2018 2017Q2-capitalbikeshare-tripdata.csv
-rw-r--r-- 1 root root 165260132 Mar 15  2018 2017Q3-capitalbikeshare-tripdata.csv
-rw-r--r-- 1 root root 112536025 Mar 15  2018 2017Q4-capitalbikeshare-tripdata.csv

2)将数据load到表中

mysql> CREATE DATABASE bikeshare;
Query OK, 0 rows affected (0.88 sec)
mysql> USE bikeshare;
Database changed
mysql> CREATE TABLE trips (
    ->  trip_id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
    ->  duration integer not null,
    ->  start_date datetime,
    ->  end_date datetime,
    ->  start_station_number integer,
    ->  start_station varchar(255),
    ->  end_station_number integer,
    ->  end_station varchar(255),
    ->  bike_number varchar(255),
    ->  member_type varchar(255)
-> );
mysql> LOAD DATA LOCAL INFILE '/usr/local/src/bikeshare-data/2017Q1-capitalbikeshare-tripdata.csv' INTO TABLE trips   FIELDS TERMINATED BY ',' ENCLOSED BY '"'   LINES TERMINATED BY '\r\n'   IGNORE 1 LINES (duration, start_date, end_date, start_station_number, start_station, end_station_number, end_station, bike_number, member_type);

3)explain语句

mysql> EXPLAIN SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-01-01 00:00:00' AND '2017-02-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           | 6.33    | cop[tikv] |               | ge(bikeshare.trips.start_date, 2017-01-01 00:00:00.000000), le(bikeshare.trips.start_date, 2017-02-01 23:59:59.000000) |
|       └─TableFullScan_18     | 253.00  | cop[tikv] | table:trips   | keep order:false, stats:pseudo                                                                                         |
+------------------------------+---------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+
5 rows in set (0.46 sec)

在上面的例子中,coprocessor上读取trips表上的数据(TableFullScan_18),寻找满足start_date BETWEEN ‘2017-01-01 00:00:00’ AND ‘2017-02-01 23:59:59’ 条件的数据(Selection_19),然后计算满足条件的数据行数(StreamAgg_9),最后把结果返回给TiDB。TiDB汇总各个coprocessor 返回的结果 (TableReader_21),并进一步计算所有数据的行数(StreamAgg_20),最终把结果返回给客户端。在上面这个查询中,TiDB根据trips表的统计信息估算出 TableScan_18 的输出结果行数为253.00,满足条件start_date BETWEEN ‘2017-01-01 00:00:00’ AND ‘2017-02-01 23:59:59’ 的有6.33条,经过聚合运算后,只有1条结果。

4)添加索引后再运行SQL语句

ALTER TABLE trips ADD INDEX (start_date);
mysql> EXPLAIN SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-01-01 00:00:00' AND '2017-02-01 23:59:59';
+-----------------------------+---------+-----------+-------------------------------------------+---------------------------------------------------------------------------------+
| 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     | 6.33    | cop[tikv] | table:trips, index:start_date(start_date) | range:[2017-01-01 00:00:00,2017-02-01 23:59:59], keep order:false, stats:pseudo |
+-----------------------------+---------+-----------+-------------------------------------------+---------------------------------------------------------------------------------+
4 rows in set (0.58 sec)

在添加完索引后的新执行计划中,使用IndexRangeScan_16直接读取满足条件start_date BETWEEN ‘2017-01-01 00:00:00’ AND ‘2017-02-01 23:59:59’ 的数据,可以看到,估算的要扫描的数据行数从之前的253降到了现在的6.33。


参考资料:

  1. https://pingcap.com/blog-cn/tidb-source-code-reading-6/
  2. https://docs.pingcap.com/zh/tidb/stable/sql-tuning-overview
  3. https://book.tidb.io/session3/chapter4/performance-map.html
  4. https://book.tidb.io/session1/chapter3/tidb-sql-layer-summary.html
  5. https://book.tidb.io/session3/chapter1/sql-execution-plan.html
  6. https://github.com/pingcap/docs/blob/master/import-example-data.md

转载请注明原文地址:https://blog.csdn.net/solihawk/article/details/118990546
文章会同步在公众号“牧羊人的方向”更新,感兴趣的可以关注公众号,谢谢!
在这里插入图片描述

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值