【华为云MySQL技术专栏】MySQL 8.0 子查询物化机制详解
1、关于子查询
在 SQL 查询中,一个查询语句可以嵌入到另一个查询语句中,这种嵌入的查询称为子查询,而包含子查询的查询则被称为外层查询。根据子查询与外层查询的关联性,子查询可以分为相关子查询(依赖于外层查询中的某些值)和非相关子查询(可以独立执行)。
子查询可以出现在外层查询的任何部分,这极大地简化了数据库操作。然而,如果优化器没有选择合适的执行计划,或者子查询使用不当,可能会显著降低执行效率,尤其是在处理大量数据时。
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 子查询物化示意图
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 使用限制
为了在 MySQL 中启用子查询物化,必须将系统变量 optimizer_switch 中的 materialization 设置为 on。启用此配置后,物化策略适用于子查询出现在任何位置(例如:SELECT 列表、WHERE、ON、GROUP BY、HAVING 或 ORDER BY)。
子查询物化支持的操作符:
存在性检查:IN、= ANY、 = SOME(三者语义等价)。
非存在性检查:NOT IN、<> ALL、!= ALL(三者语义等价)。
要利用物化优化子查询,查询语句必须满足以下格式要求:
-
外层查询中 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 关键数据结构类图
1) Item_in_subselect
Item_in_subselect用于表示形如left_expr [NOT] IN (SELECT ...)的谓词结构,继承自Item_exists_subselect,其核心功能是对依赖子查询结果的存在性(即 IN 或 NOT IN)进行判断。
子查询物化涉及的关键成员变量如表1:
表1 Item_in_subselect类关键成员变量
子查询物化涉及的关键成员方法如表2:
表2 Item_in_subselect类关键成员方法
2)Item_in_optimizer
Item_in_optimizer是用于包装Item_in_subselect 实例的类。
Item_in_optimizer 在Item_in_subselect 的基础上添加了额外的功能。这些功能包括对外层表达式值的缓存、NULL 情况的快捷处理以及与整个查询求值流程的集成。
子查询物化涉及的关键成员变量如表3:
表3 Item_in_optimizer类关键成员变量
子查询物化涉及的关键成员方法如表4:
表4 Item_in_optimizer类关键成员方法
3)subselect_hash_sj_engine
subselect_hash_sj_engine 继承自subselect_indexsubquery_engine,复用基础子查询引擎功能,但扩展了 哈希半连接 的实现,是 MySQL 中处理 非相关子查询物化 的核心引擎类,专为优化 IN 或 NOT IN 子查询设计。
子查询物化涉及的关键成员变量如表5:
表5 subselect_hash_sj_engine类关键成员变量
子查询物化涉及的关键成员方法如表6:
表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,将结果写入物化临时表,并做去重处理;
(3)针对左表达式(left_expr)的缓存处理,避免重复访问物化临时表,提升性能;
(4)针对NULL 场景的特殊处理,确保IN/NOT IN 的结果符合 SQL 标准。
5、总结
以上文章重点介绍了MySQL 8.0子查询物化优化机制的原理、适用场景和使用限制,并对8.0.22版本的代码实现进行了解析。子查询物化通过缓存结果集和索引优化,显著提升非相关子查询性能,尤其适用于大结果集和多次引用场景。用户在使用子查询需注意NULL值、类型匹配等,以辅助优化器决策。对于复杂查询,也需结合Semijoin/Antijoin、EXISTS转换等策略综合优化。
- 点赞
- 收藏
- 关注作者
评论(0)