【华为云MySQL技术专栏】MySQL 8.0 子查询物化机制详解

举报
GaussDB 数据库 发表于 2025/05/26 09:30:32 2025/05/26
【摘要】 1、关于子查询在 SQL 查询中,一个查询语句可以嵌入到另一个查询语句中,这种嵌入的查询称为子查询,而包含子查询的查询则被称为外层查询。根据子查询与外层查询的关联性,子查询可以分为相关子查询(依赖于外层查询中的某些值)和非相关子查询(可以独立执行)。子查询可以出现在外层查询的任何部分,这极大地简化了数据库操作。然而,如果优化器没有选择合适的执行计划,或者子查询使用不当,可能会显著降低执行效率...

MySQL顶部banner.jpg

1、关于子查询

在 SQL 查询中,一个查询语句可以嵌入到另一个查询语句中,这种嵌入的查询称为子查询,而包含子查询的查询则被称为外层查询。根据子查询与外层查询的关联性,子查询可以分为相关子查询(依赖于外层查询中的某些值)和非相关子查询(可以独立执行)。

子查询可以出现在外层查询的任何部分,这极大地简化了数据库操作。然而,如果优化器没有选择合适的执行计划,或者子查询使用不当,可能会显著降低执行效率,尤其是在处理大量数据时。

针对不同类型的子查询,MySQL 优化器有五种最终的执行方式,其中`SUBQ_MATERIALIZATION` 是一种重要的执行方式。本文将结合源码,详细解析 MySQL 8.0.22 版本中非相关子查询的物化执行(即`Subquery_strategy::SUBQ_MATERIALIZATION`)的原理、实现、应用场景和限制。

2、子查询物化优化原理

MySQL优化器为了提高子查询的执行效率,采用了“物化”(Materialize)策略,即将子查询的结果提前计算并存储到一个临时表中,后续在使用该结果时直接引用这个临时表,而不必每次重新计算子查询。

其实现原理如下:

1)生成物化表

当外层查询首次需要子查询的结果时,优化器会立即执行该子查询,并将得到的结果集写入一个临时表中。这个临时表的列结构与子查询返回的列一致。

2)记录去重

写入物化表时,会对记录进行去重处理。对于 IN 语句来说,其作用仅在于判断某个操作数是否存在于指定集合中,集合中值是否重复并不会影响最终判断结果。因此,去除重复记录不仅能减少物化临时表的内存占用,还能够加速后续查询操作。

3)内存与磁盘的选择

  • 内存存储和哈希索引:

    在大多数情况下,子查询的结果集不会非常庞大,MySQL 会使用基于内存的临时表来存储物化表。此时,为了加速查找,物化表会建立哈希索引,该索引不仅能快速定位记录,还能确保记录唯一性。

  • 磁盘存储和B+树索引:

    如果子查询的结果集超过了系统变量tmp_table_size或max_heap_table_size限定的内存容量,临时表就会自动转换为基于磁盘的存储引擎。在这种情形下,索引类型也会转变为B+ 树索引,以便高效地管理大量数据。

4)后续引用

物化临时表创建成功后,后续需要访问子查询的地方,MySQL都会直接引用这个物化表,从而避免重复计算。物化表的索引使得 IN 语句判断过程非常高效,大幅提升SQL语句查询的性能。

以下面的SQL语句为例:

SELECT * FROM t1 WHERE t1.m IN (SELECT t2.m FROM t2);

其物化执行的过程,具体如图1所示:

1.PNG

子查询物化示意图

3、应用场景和限制

3.1 适应场景

子查询物化是 MySQL 针对那些无法直接转换为 Semijoin(例如 IN)或 Antijoin(例如NOT IN)的非相关子查询采取的一种优化策略。当非相关子查询无法等价转换为 EXISTS 形式,或虽然可以转换为EXISTS 形式但其转换开销高于物化成本时,MySQL 会选择子查询物化方式来优化子查询性能,从而提升整体查询性能。

以下面SQL语句为例:

mysql> EXPLAIN SELECT * FROM t1 WHERE a IN (SELECT MAX(a) FROM t1 GROUP BY id);
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra           
|+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
|  1 | PRIMARY     | t1    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    6 |   100.00 | Using where     |
|  2 | SUBQUERY    | t1    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    6 |   100.00 | Using temporary |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
2 rows in set, 1 warning (0.01 sec)

由执行计划可知,优化器选择了子查询物化的方式执行,原因如下:

1)子查询存在聚合函数和GROUP BY,无法走semijoin;

(2)子查询满足物化执行条件;

(3)优化器估算EXISTS方式执行代价大于子查询物化执行代价。

补充说明:

subquery_to_derived优化选项默认关闭,本案例不考虑subquery_to_derived开启场景。

3.2 使用限制


1)特性开关

为了在 MySQL 中启用子查询物化,必须将系统变量 optimizer_switch 中的 materialization 设置为 on。启用此配置后,物化策略适用于子查询出现在任何位置(例如:SELECT 列表、WHERE、ON、GROUP BY、HAVING 或 ORDER BY)。

2)支持的操作符

子查询物化支持的操作符:

存在性检查:IN、= ANY = SOME(三者语义等价)。

非存在性检查NOT IN、<> ALL、!= ALL(三者语义等价)。


3)语句格式要求

要利用物化优化子查询,查询语句必须满足以下格式要求:

  • 外层查询中 oe_i 和内部子查询 ie_i 不能为 nullable,且N是1或者更大的值。

(oe_1, oe_2, ..., oe_N) [NOT] IN (SELECT ie_1, i_2, ..., ie_N ...)
  • 外层查询和内部子查询均只有一个表达式,表达式的值可以为 nullable。

oe [NOT] IN (SELECT ie ...)
  • 当谓词为 IN  NOT IN 时,UNKNOWN(即NULL)的结果与 FALSE 的结果具有相同的含义。因此,如果某一列在实际中永远不会为 NULL,那么将该列声明为 NOT NULL 能够协助优化器进行更有效的优化。

4. 列的数据类型要求

关于列的数据类型,必须满足如下条件才能应用子查询物化:

  • 内部子查询与外层查询中各对应列的类型必须严格匹配。例如,如果一个列是 integer,而另一个是 decimal,则优化器将无法使用子查询物化。

  • 内部子查询的表达式类型不能为LOB字段(BLOB/TEXT/JSON/GEOMETRY/…)。

4、源码解析

4.1 关键数据结构

关键数据结构示意如下图2所示:

2.PNG

关键数据结构类图

1) Item_in_subselect

Item_in_subselect用于表示形如left_expr [NOT] IN (SELECT ...)的谓词结构,继承自Item_exists_subselect,其核心功能是对依赖子查询结果的存在性(即 IN 或 NOT IN)进行判断。

子查询物化涉及的关键成员变量如表1:

11.PNG1 Item_in_subselect类关键成员变量

子查询物化涉及的关键成员方法如表2:

22.PNG

2 Item_in_subselect类关键成员方法

2)Item_in_optimizer

Item_in_optimizer是用于包装Item_in_subselect 实例的类。

Item_in_optimizer 在Item_in_subselect 的基础上添加了额外的功能。这些功能包括对外层表达式值的缓存、NULL 情况的快捷处理以及与整个查询求值流程的集成。

子查询物化涉及的关键成员变量如表3:

33.PNG

3 Item_in_optimizer类关键成员变量

子查询物化涉及的关键成员方法如表4:

44.PNG

4 Item_in_optimizer类关键成员方法

3)subselect_hash_sj_engine

subselect_hash_sj_engine 继承自subselect_indexsubquery_engine,复用基础子查询引擎功能,但扩展了 哈希半连接 的实现,是 MySQL 中处理 非相关子查询物化 的核心引擎类,专为优化 IN 或 NOT IN 子查询设计。

子查询物化涉及的关键成员变量如表5:

55.PNG

5 subselect_hash_sj_engine类关键成员变量

子查询物化涉及的关键成员方法如表6:

66.PNG

表6 subselect_hash_sj_engine类关键成员方法

4.2 流程解析

  • Prepare阶段

在prepare阶段之前,已完成SQL语句的parse处理。Prepare阶段主要完成对抽象语法树的resolve,以及基于启发式规则来做 query transformation。

Prepare阶段,子查询物化相关的处理流程如下:

|--> SELECT_LEX::prepare  // 当前SELECT_LEX为子查询
  ...
  |--> SELECT_LEX::remove_redundant_subquery_clauses
        1. 对于表子查询谓词(IN/ANY/ALL/EXISTS等),删除ORDER BY子句、DISTINCT子
        2. 对于 无 LIMIT 的标量子查询,删除ORDER BY子句
  |--> SELECT_LEX::resolve_subquery
    |--> 对于IN/EXIST subselect,判断是否可以转换为semi-join/anti-join,如果可以,设置strategy为CANDIDATE_FOR_SEMIJOIN
    |--> 否则,判断IN/EXIST subselect是否可以转换为derived_table,如果可以,设置strategy为CANDIDATE_FOR_DERIVED_TABLE
    |--> Item_in_subselect::select_transformer  // 上面的转换条件都不满足,是适用于子查询(IN/ANY/ALL/SOME)
      |--> Item_in_subselect::select_in_like_transformer
        |--> 创建Item_in_optimizer,即Item_in_subselect的封装类
        |--> 设置strategy为 CANDIDATE_FOR_IN2EXISTS_OR_MAT,表示尚不确定其执行方式是EXISTS还是物化
        |--> Item_in_subselect::single_value_transformer实现ANY/ALL转换为MIN/MAX子查询,以及IN转换为EXIST的准备
        |--> 如果left_expr只有一列,调用Item_in_subselect::single_value_transformer
               1. 实现ANY/ALL转换为MIN/MAX子查询
               2. IN转换为EXIST的准备
        |-->如果left_expr有多列,就变成一个row而不是一个标量,调用Item_in_subselect::row_value_transformer
            逻辑与Item_in_subselect::single_value_transformer一致
  |--> SELECT_LEX::flatten_subqueries 
        对于之前收集的可以转换为semi-join/anti-join/derived_table的subquery,尝试转换

经过prepare阶段的处理,对于非相关的IN子查询而言,如果能够转为semi-join/anti-join/derived_table,则已完成转换;否则,strategy已置为 CANDIDATE_FOR_IN2EXISTS_OR_MAT,待进一步确认执行方式是物化还是EXISTS。

  • Optimize阶段

在优化阶段,MySQL会基于代价估算,决定CANDIDATE_FOR_IN2EXISTS_OR_MAT的子查询,是物化还是EXISTS方式执行。

Optimize阶段,子查询物化相关的处理流程:

|--> SELECT_LEX_UNIT::optimize  // 子查询对应的query expression
  |--> SELECT_LEX::optimize    
    |--> JOIN::optimize
      ...
      |--> JOIN::make_join_plan
        |--> 如果是子查询,调用JOIN::decide_subquery_strategy
          |--> 如果是IN/ANY/ALL子查询,且CANDIDATE_FOR_IN2EXISTS_OR_MAT,调用JOIN::compare_costs_of_subquery_strategies
            |--> 如果子查询包含非确定性元素、外部引用、不可物化结构等,则不能采用物化方式执行
            |--> Item_in_subselect::subquery_allows_materialization  // 硬性限制,无法做物化
            |--> calculate_materialization_costs // 计算物化的代价
            |--> calculate_subquery_executions   // 计算exists子查询的执行次数
            |--> 对比cost_mat和cost_exists,如果cost_mat < cost_exists,选择物化执行,并设置strategy为SUBQ_MATERIALIZATION
          |--> Item_in_subselect::finalize_materialization_transform  // 选择物化执行
            |--> Item_in_subselect::remove_in2exists_conds // 删除在prepare阶段,为exist转换注入的额外cond
            |--> 创建 subselect_hash_sj_engine
            |--> subselect_hash_sj_engine::setup
              |--> 创建Query_result_union
              |--> Query_result_union::create_result_table // 创建物化的临时表,保存在Query_result_union::table上
              |--> 在物化的临时表上建索引
              |--> 构建对应QEP_TAB/TABLE/TABLE_REF对象,便于后续读取
  |--> Item_subselect::create_iterators
    |--> Item_subselect::create_iterators
      |--> subselect_hash_sj_engine::create_iterators
          创建访问物化表的迭代访问计划和子查询的迭代访问计划,分别保存在MaterializeIterator::m_table_iterator和MaterializeIterator::QueryBlock::subquery_iterator。

对于非相关的IN子查询而言,optimize阶段明确最终选择物化还是EXISTS方式执行。如果选择物化执行,则创建物化临时表,并创建物化临时表的迭代访问计划。

  • Execute阶段

子查询物化执行会由所在的条件/投影列的计算作为入口,调用Item_in_optimizer::val_int方法来执行。

Execute阶段,子查询物化相关的处理流程:


|--> Item_in_optimizer::val_int
  |--> 计算左表达式的值存入缓存,标记是否为 NULL
  |--> 左表达式的缓存值为 NULL,做特殊处理
  |--> Item_in_subselect::val_bool_naked
    |--> Item_in_subselect::exec
       |--> 如果left_expr_cache未初始化,调用Item_in_subselect::init_left_expr_cache
           为 IN 子查询的左表达式(left_expr)初始化缓存机制,避免在外层查询的多次迭代中重复计算左表达式的值。
       |--> 如果left_expr_cache已初始化,调用update_item_cache_if_changed函数
           检查左表达式(left_expr)单行缓存中的项是否发生变化,并更新缓存
       |--> 如果当前新值与left_expr_cache缓存的旧值一样,则直接复用缓存的前一次子查询执行结果,提前返回
       |--> Item_subselect::exec
         |--> subselect_hash_sj_engine::exec
           |--> 如果subquery未物化,调用MaterializeIterator::Init完成物化操作
             |--> 遍历m_query_blocks_to_materialize这个数组,针对其中每个query block,调用MaterializeIterator::MaterializeQueryBlock
               |--> 每个query block对象也包含一个subquery_iterator,是对应子查询优化后的root iterator,触发执行流程,结果写入到物化table中
           |--> ExecuteExistsQuery  // subquery已物化,判断是否存在至少一行匹配结果
             |--> MaterializeIterator::Read // 检查左表达式(left_expr)的结果值是否在物化临时表中,触发执行MaterializeIterator::m_table_iterator。
    |--> 当子查询的执行过程中存在NULL值,且子查询的原始结果为false时,将最终结果标记为NULL

对于非相关的IN子查询而言,execute阶段完成以下处理:

(1)在外层查询首次需要子查询的结果时,执行子查询迭代计划MaterializeIterator::QueryBlock::subquery_iterator,将结果写入物化临时表,并做去重处理;

(2)在外层查询每次需要子查询的结果时,执行MaterializeIterator::m_table_iterator,判断是否存在至少一行匹配结果;

(3)针对左表达式(left_expr)的缓存处理,避免重复访问物化临时表,提升性能;

(4)针对NULL 场景的特殊处理,确保IN/NOT IN 的结果符合 SQL 标准。

5、总结

以上文章重点介绍了MySQL 8.0子查询物化优化机制的原理、适用场景和使用限制,并对8.0.22版本的代码实现进行了解析。子查询物化通过缓存结果集和索引优化,显著提升非相关子查询性能,尤其适用于大结果集和多次引用场景。用户在使用子查询需注意NULL值、类型匹配等,以辅助优化器决策。对于复杂查询,也需结合Semijoin/AntijoinEXISTS转换等策略综合优化。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。