列存索引相关算子说明

本文介绍了使用列存索引的过程中会使用到的所有算子及其说明内容,您可以在通过EXPLAIN查看执行计划时查看到这些算子。

EXPLAIN查询结果格式以及含义

EXPLAIN查询结果中包含以下列:

列名称

含义

ID

每一行的序号。

Operator

查询计划中算子的名称。

Name

扫描的表的表名。

说明

仅当算子为Table Scan时会显示该字段。

E-Rows

Estimate Rows,优化器预估该算子返回的数据行数。

E-Costs

Estimate Cost,优化器预估该算子执行的代价。该值为累加值,每个算子节点的代价包括其子节点的代价。

Extra Info

算子的额外信息。不同的算子这部分信息不同,有助于进一步了解执行计划相关的信息。

列存查询计划的EXPLAIN查询结果中使用树形结构展示查询计划。查询计划的每一行可以看作一个树上的节点,其拥有一个父节点和若干子节点,在执行查询操作时,总是子节点先于父节点执行。若几个节点处于查询计划的同一层,那么ID小的节点先执行。以一个简单的查询计划举例:

+----+----------------------+------+--------+--------+---------------------------------------------------------------+
| ID | Operator             | Name | E-Rows | E-Cost | Extra Info                                                    |
+----+----------------------+------+--------+--------+---------------------------------------------------------------+
|  1 | Select Statement     |      |        |        | IMCI Execution Plan (max_dop = 32, max_query_mem = unlimited) |
|  2 | └─Hash Groupby       |      | 30     | 0.25   | Group Key: t1.a                                               |
|  3 |   └─Hash Join        |      | 30     | 0.15   | Join Cond: t1.a = t2.a                                        |
|  4 |     ├─Table Scan     | t1   | 30     | 0.00   |                                                               |
|  5 |     └─Table Scan     | t2   | 30     | 0.00   |                                                               |
+----+----------------------+------+--------+--------+---------------------------------------------------------------+

在这个查询计划中,执行的顺序如下:

  1. 执行4,扫描t1表中的所有数据,并构建哈希表。

  2. 执行5,扫描t2表中的所有数据。

  3. 执行3,使用t2表中的数据进行查找之前,先使用t1表构建的哈希表进行哈希连接。

  4. 使用哈希连接的结果进行哈希分组,返回数据。

通过EXPLAIN获得的查询计划,可以通过算子的预估行数与代价估算查询耗时及性能瓶颈。结合列存索引的查询性能分析功能,能够对查询性能进行详细分析,详见分析列存索引查询性能

相关算子

Table Scan

Table Scan用于扫描表的所有行,通过谓词(若有)对每一行进行过滤。

Extra Info字段中可能包含以下内容:

  • Cond:扫描过程中用于过滤的谓词。

  • Partitions:扫描分区表过程中,如果只扫描了部分分区,该字段将展示这些分区的名称。

  • DynamicFilter:由Hash Join的Hash Build端构造并下推的动态过滤器。

Constant Scan

Constant Scan用于扫描常量表中的所有行并返回结果。常量表可能由以下情形生成:

  • IN谓词中IN-List长度过长时,如col IN (1, 2, ..., 100),IN-List将会被转换为semi join以加速谓词执行。此时将生成包含(1, 2, ..., 100)这100行的常量表用于执行查询操作。

  • 部分SQL语句中包含常量,如SELECT 1;,优化器将生成一张仅有一行的常量表用于驱动查询并返回结果。

  • 如果查询中的部分子查询在优化中被识别出其不可能返回结果,那么子查询将会被一张空的常量表替代,用于加速查询。

Extra Info字段中可能包含以下内容:

  • Empty Table:表示该常量表是一张空表。

  • Cte Table:表示该表是用于缓存递归公共表达式(Recursive CTE)结果的常量表。

Filter/Assert

Filter通过谓词过滤查询中不符合条件的行,最后仅返回符合条件的部分。

Assert是Filter的一个变种,若存在输入不满足断言的谓词条件,查询不会将这一行丢弃掉,而是直接返回一个错误,通常断言由子查询去关联的变换过程产生。

Extra Info字段中可能包含以下内容:

Cond:用于过滤 / 断言的谓词。

Hash Join

Hash Join通过等值条件连接两张表,执行时先读取build端的数据,然后使用等值条件相关的列构建哈希表,随后读取probe端的数据。即在哈希表中查找并连接匹配等值条件的行。

列存索引中的哈希连接支持左/右外连接(Left/Right Outer),以及左/右半连接(Left/Right Semi)。这些信息也会显示在查询计划的算子名称中,例如Hash Left Semi Join

哈希连接支持阻塞(Blocking)与流水线(Pipeline)两种执行模式,两者的主要区别为该算子与下游算子的执行顺序:

  • Blocking模式

    先将当前算子的所有结果写进临时表,下游的算子从临时表消费数据。Blocking模式相当于“切断”了流水线,在一条流水线中由于同时执行的算子减少,内存不充裕时每个算子可用的内存会增加。

  • Pipeline模式

    下游算子从当前算子直接拉取数据。Pipeline模式没有临时表缓存结果的开销,在内存足够充裕的情况下能够获得更好的执行性能。

Extra Info字段中可能包含以下内容:

  • IsBlocking:查询是否以Blocking模式执行。

  • Join Cond:用于进行哈希连接的等值谓词。

  • PostJoin Filter:连接条件中,除了等值谓词以外的其他谓词。例如t1 LEFT JOIN t2 ON t1.c1 = t2.c1 AND t1.c2 > t2.c2,其中t1.c2 > t2.c2就是PostJoin Filter。

  • SemiProbe:表示该查询是半连接(semi join)的变种。在该模式下,probe端进行probe时,连接将不会通过谓词过滤数据,而是返回该行数据对应谓词的结果。例如t1 LEFT SEMI JOIN(SEMIPROBE) t2 ON t1.a = t2.a,其中t1t2均只有一行,分别为t1: (a: 1), t2: (a: 2)。如果是普通的半连接,该连接不返回任何结果。如果在SemiProbe模式下,该连接将返回一行(1, False),其中False表示该行没有与t2表中的任何数据匹配。该变种主要出现在复杂的子查询中。

  • DynamicFilter:由Hash Join的Hash Build端构造的动态过滤器信息。

Nested Loop Join/Cartesian Product

嵌套循环连接(Nested Loop Join)使用嵌套循环的方式连接两张表,其中一张表作为驱动表,另一张表作为被驱动表,这种连接方式会遍历驱动表,使用其中的每一行遍历整张被驱动表来查找满足连接谓词的行。该算法效率通常较Hash Join差,通常用于不满足Hash Join使用前提的场景。

笛卡尔积(Cartesian Product)是Nested Loop Join的一个特例,属于内连接且没有连接谓词,即所有行均满足连接条件。

与Hash Join类似,嵌套循环连接也支持左右外连接与半连接。

Extra Info字段中可能包含以下内容:

  • Join Cond:用于哈希连接的谓词。

  • SemiProbe:表示该查询是半连接(semi join)的变种,具体可参考Hash Join中的相关解释。

Union/Union All

并集(Union)用于合并多个查询的输入,并输出最终结果。

Union与Union All的区别在于,Union会对结果进行去重,而Union All不会。列存索引中使用Union All+Hash Groupby来实现Union,所以通常执行计划中不会有Union。

Hash Groupby/Aggregation

哈希分组(Hash Groupby)用于执行查询中的GROUP BY子句。该算子通过哈希表将数据集按照一个或多个列的值进行分组,并在每个组上执行聚合函数(如SUM、COUNT、AVG、MAX或MIN等)。

聚合(Aggregation)没有分组列,属于Hash Groupby的一个特例。

Extra Info字段中可能包含以下内容:

Group Key:用于分组的列。

Group Join

分组连接(Group Join)是对同时包括连接与分组查询的优化。如果连接所用的列与分组列相同,例如:

SELECT t1.a, COUNT(*) FROM t1 JOIN t2 ON t1.a = t2.a GROUP BY t1.a -- JOIN与GROUP BY均使用了t1.a

则可以通过共用同一个哈希表来达到加速查询的作用,在列存索引中,通过分组连接的方式实现这个优化,以下是该SQL优化前后的执行计划区别。

  • 普通的Hash Join+Groupby。

    +----+----------------------+------+------------------------+
    | ID | Operator             | Name | Extra Info             |
    +----+----------------------+------+------------------------+
    |  1 | Select Statement     |      |                        |
    |  2 | └─Hash Groupby       |      | Group Key: t1.a        |
    |  3 |   └─Hash Join        |      | Join Cond: t2.a = t1.a |
    |  4 |     ├─Table Scan     | t2   |                        |
    |  5 |     └─Table Scan     | t1   |                        |
    +----+----------------------+------+------------------------+
  • 通过GroupJoin合并Hash Join+Groupby优化执行效率。

    +----+--------------------+------+-----------------------------------------+
    | ID | Operator           | Name | Extra Info                              |
    +----+--------------------+------+-----------------------------------------+
    |  1 | Select Statement   |      |                                         |
    |  2 | └─Group Join       |      | Join Cond: t2.a = t1.a, Group Key: t1.a |
    |  3 |   ├─Table Scan     | t2   |                                         |
    |  4 |   └─Table Scan     | t1   |                                         |
    +----+--------------------+------+-----------------------------------------+

与Hash Join类似,分组连接也支持左右外连接与半连接。

Extra Info字段中可能包含以下内容:

  • Join Cond:用于哈希连接的谓词。

  • Group Key:用于分组的列,用于分组的列是唯一键,会带有unique表明唯一性。

Window

窗口函数(Window)在保持原有数据集不变的情况下,在数据集中数据对应的“窗口(Frame)”中执行聚合操作或其他复杂运算,该窗口由当前行及其上下文行(根据指定规则确定)组成。

说明

一些简单的窗口函数可能不会使用window执行,而是使用groupby+join。

Extra Info字段中可能包含以下内容:

  • Partition Key:用于分区的列。相当于窗口函数中的PARTITION BY子句,如果不存在分区键,那么整个数据集将被视为一个分区。

  • Sort Key:分区内用于排序的列。相当于窗口函数中的ORDER BY子句。

  • Frame:进一步定义窗口的范围。

Expand

扩展(Expand)操作和Hash Groupby配合来实现查询中WITH ROLLUP子句的功能。对于数据集中的每一行,扩展操作会根据分组键的列数输出多行用于执行ROLLUP。示例如下:

SELECT SUM(a) FROM t1 GROUP BY b, c WITH ROLLUP;

假设其中一行的数据是(a, b, c) = (1, 2, 3),扩展操作会输出3行:

Expand(1, 2, 3) = [(1, 2, 3), (1, 2, NULL), (1, NULL, NULL)]

通过将对应列设置为NULL后再做分组,来实现WITH ROLLUP逐层聚合的功能。

Extra Info字段中可能包含以下内容:

Expand Exprs:和Hash Groupby中的Group Key一致,若Expand Exprsk个,那么Expand将把每一行扩展为k+1行(对应上面示例中的k=2)。

Table Spool

假脱机(Table Spool)操作用于在查询执行过程中对数据进行临时存储和重用,在查询中其分为以下两种:

  • Primary Table Spool:该算子接受输入,并且缓存到数据库内部的临时表中。

  • Ref Table Spool:该算子从Primary Table Spool对应的临时表中读取数据并输出。

以下是一个使用Table Spool的查询的示例:

EXPLAIN SELECT SUM(DISTINCT a), SUM(DISTINCT b) FROM t1;
+----+-----------------------------------+------+-----------------+
| ID | Operator                          | Name | Extra Info      |
+----+-----------------------------------+------+-----------------+
|  1 | Select Statement                  |      |                 |
|  2 | └─Cartesian Product               |      |                 |
|  3 |   ├─Aggregation                   |      |                 |
|  4 |   │ └─Hash Groupby                |      | Group Key: t1.a |
|  5 |   │   └─Primary Table Spool       |      |                 |
|  6 |   │     └─Table Scan              | t1   |                 |
|  7 |   └─Aggregation                   |      |                 |
|  8 |     └─Hash Groupby                |      | Group Key: t1.b |
|  9 |       └─Ref Table Spool           |      | Ref: 5          |
+----+-----------------------------------+------+-----------------+

其中,第9行的Ref Table Spool引用了第5行的Primary Table Spool产生的临时表。

从下图可以直观地看到该查询的执行计划:

image

Extra Info字段中可能包含以下内容:

Ref:表示引用了查询计划中第几行的Primary Table Spool

Sort

排序(Sort)用于对数据进行排序。

Extra Info字段中可能包含以下内容:

Sort Key:排序使用的列。每列按照列名 升序/降序表示,如t1.col ASC,t2.col DESC

Limit

Limit算子接受输入,但只返回指定的行数。

Extra Info字段中可能包含以下内容:

  • Limit:返回结果的最大行数。

  • Offset:返回结果开始前跳过的行数。

Max1Row

Max1Row是一个断言,用于保证标量子查询仅返回一行结果,否则将抛出一个Subquery returns more than 1 row错误。

Create Unique Key

Create Unique Key会为子查询创建一个整型的非空唯一键,通常会在子查询去关联时生成。

Compute Scalar

计算标量(Compute Scalar)接受输入,返回一行通过表达式计算得到的标量值。

HybridIndexSearch

混合索引查找(HybridIndexSearch)使用主键查询行存索引以加速宽表上的明细查询。详细文档可以参考使用Hybrid Plan加速宽表查询

Extra Info字段中可能包含以下内容:

Used table:利用行存索引加速明细查询的表。

RecursiveCte

递归公共表达式(Recursive CTE)允许用户执行递归查询,即在查询中自我引用以生成一系列相关的行。递归通过多次迭代自身来构建结果集,直到达到某个终止条件为止。其分为以下两部分:

  • 非递归部分(Base Case/Anchor Member):查询的起始点,返回一组初始数据行,构成了递归的第一层或基础数据。这部分查询不依赖于CTE本身。

  • 递归部分(Recursive Member):这一部分通过引用CTE的名称来引用前一次迭代的结果,并在此基础上进一步扩展结果集。每一次迭代都会生成更多行,直至满足停止递归的条件为止。递归部分通常会使用非递归部分返回的数据,或者基于之前的结果进行某种变换。

以递归公共表达式为例:

WITH Recursive_CTE AS (
    -- 非递归部分:基础成员,其结果记为{A}
    SELECT column_list
    FROM table_name
    WHERE condition_for_base_case
    
    UNION ALL
    
    -- 递归部分:递归成员,引用CTE名称,其结果记为{B1}, {B2}, ..., {Bi}
    SELECT column_list
    FROM table_name
    JOIN Recursive_CTE ON join_condition
    WHERE condition_to_continue_recursive
)
SELECT * FROM Recursive_CTE;

可以看到,递归部分引用了CTE自身,CTE在递归部分被引用时,其值为:

  • 第一次执行递归部分时,引用的CTE值为非递归部分查询的结果image.svg,产生的结果为image.svg

  • 第二次执行递归部分时,引用的CTE值为上次产生的结果image.svg,本次产生的结果为image.svg

  • ......

  • 第k次执行递归部分时,引用的CTE值为上次产生的结果image.svg,本次产生的结果是image.svg

递归在递归查询结果image.svg终止。

在包含递归公共表达式的查询计划中,会看到其包含一个Cte Table,表示该表是用于缓存递归公共表达式中CTE上次查询结果的常量表。

Extra Info字段中可能包含以下内容:

  • Left Exprs:非递归部分查询结果包含的列。

  • Right Exprs:递归部分查询结果包含的列。

JsonTable

JsonTable函数是一种特殊的表函数,它的目的是将JSON文档转换成关系型数据库表的形式,以便于用户能够像操作普通数据库表那样来查询和处理JSON数据。具体描述和用法可以参考MySQL官方文档

Extra Info字段中可能包含以下内容:

  • JsonExpr:输入的JSON表达式。可以是表的某一列或常量字符串,也可以是某个表达式返回的结果(例如(JSON_EXTRACT(t1.json_data,'$.post.comments')))。

  • PathExpr:指定JSON中要提取数据的路径。JSON中相关路径下的数据将用于生成输出的关系表中的数据。

  • ChildExprs:包含JSON表指定的所有列与其算子输出的所有列。

Exchange与Consume

Exchange与Consume是在多机并行模式下才会出现的算子,用于对数据进行重分布。

其中,Exchange接受多个机器上的输入,并将这些输入按照一定的规则分发到一个或多个机器上。当前支持以下三种规则:

  • 广播(Boardcast):将数据广播到所有机器上,所有机器都拥有完整的数据。

  • 重分布(Hash):对多个机器上的输入进行分区,按照分区分发到对应的分区上,通常通过哈希值确定分发的分区。

  • 汇聚(Gather):将多个机器上的输入合并到一台机器上。

Extra Info字段中可能包含以下内容:

  • PipeId:分布式计划被切分成若干流水线进行调度,该参数表示当前流水线的ID。

  • Producers&Consumers:重分布时数据的发出方ID和接收方ID。

  • Part Type:分发规则。

  • Partition Key:如果是重分布,该字段表示用于分区的列。