MySQL优化器在处理包含子查询嵌套的SQL语句时,会优先对子查询结构进行合法性校验,随后根据子查询的类型、嵌套层级和上下文关系,选择最合适的优化策略,核心处理方式包括子查询重写和子查询物化两类。
子查询的基本分类
MySQL将子查询按照返回结果和执行时机分为不同类别,这是优化器选择处理策略的基础:
- 标量子查询:返回单个值的子查询,通常出现在SELECT列表、WHERE条件的比较表达式中
- 行子查询:返回单行的多列结果的子查询
- 列子查询:返回单列多行结果的子查询,常配合IN、ANY、ALL等操作符使用
- 表子查询:返回多行多列结果的子查询,通常出现在FROM子句中作为派生表
子查询重写机制
子查询重写是优化器将子查询转换为更高效的等价查询形式的过程,常见的重写规则包括以下几种:
1. 子查询合并
当多个子查询的条件可以合并时,优化器会将它们合并为一个子查询,减少执行次数。例如两个IN子查询可以合并为一个IN列表查询:
-- 原查询 SELECT * FROM t1 WHERE id IN (SELECT id FROM t2 WHERE type = 1) OR id IN (SELECT id FROM t2 WHERE type = 2); -- 重写后查询 SELECT * FROM t1 WHERE id IN (SELECT id FROM t2 WHERE type = 1 OR type = 2);
2. 子查询展开
对于出现在WHERE条件中的子查询,优化器会尝试将其展开为半连接(Semi Join)或者反连接(Anti Join),避免子查询的重复执行。例如IN子查询展开为半连接:
-- 原查询 SELECT * FROM t1 WHERE t1.id IN (SELECT t2.t1_id FROM t2 WHERE t2.status = 1); -- 重写后等效逻辑 SELECT t1.* FROM t1 SEMI JOIN t2 ON t1.id = t2.t1_id WHERE t2.status = 1;
3. 派生表合并
当FROM子句中的子查询(派生表)没有聚合、GROUP BY、LIMIT等操作时,优化器会将派生表合并到外层查询中,减少中间结果集的生成:
-- 原查询 SELECT t1.name, d.max_score FROM t1 JOIN (SELECT user_id, MAX(score) AS max_score FROM t3 GROUP BY user_id) d ON t1.id = d.user_id; -- 如果t3没有GROUP BY等限制,可能合并为 SELECT t1.name, t3.score AS max_score FROM t1 JOIN t3 ON t1.id = t3.user_id WHERE ...;
子查询物化机制
当子查询无法被重写或者重写后效率更低时,优化器会选择物化策略,将子查询的结果存储到临时表中,避免重复执行子查询。
物化的触发条件
通常以下场景会触发子查询物化:
- 子查询是IN或者EXISTS类型的非相关子查询,且无法展开为连接
- 子查询的结果集较小,适合存储到内存临时表
- 子查询被多次引用,物化后可以复用结果
物化的执行流程
物化过程分为两步:
- 执行子查询,将结果集写入临时表,通常会对临时表的关联列建立哈希索引,提升后续匹配效率
- 外层查询执行时,直接使用临时表的结果进行匹配,不再重复执行子查询
下面是一个物化场景的示例:
-- 该查询中t2的子查询可能被物化 SELECT * FROM t1 WHERE t1.id IN (SELECT t2.t1_id FROM t2 WHERE t2.create_time > '2024-01-01');
物化的临时表类型
优化器会根据子查询结果的大小选择临时表类型:
| 临时表类型 | 适用场景 | 特点 |
|---|---|---|
| 内存临时表 | 结果集较小,未超过tmp_table_size配置 | 读写速度快,服务器重启后丢失 |
| 磁盘临时表 | 结果集较大,超过内存临时表限制 | 使用磁盘存储,速度较慢,通常为InnoDB或MyISAM引擎 |
嵌套子查询的处理逻辑
当子查询存在多层嵌套时,优化器会自内向外逐层处理:
- 先处理最内层的子查询,判断是否可以重写或者物化
- 将内层子查询的处理结果作为外层子查询的输入,继续处理外层子查询
- 重复上述过程,直到所有嵌套层级的子查询都处理完成,生成最终的执行计划
例如三层嵌套的子查询处理示例:
-- 三层嵌套子查询
SELECT * FROM t1 WHERE id IN (
SELECT t2.t1_id FROM t2 WHERE t2.type IN (
SELECT t3.type FROM t3 WHERE t3.level = 1
)
);
-- 处理流程:先处理最内层t3的子查询,再处理t2的子查询,最后处理t1的外层查询
优化建议
基于优化器的处理机制,编写子查询时可以遵循以下建议提升性能:
- 尽量避免多层嵌套的子查询,优先使用连接查询替代
- 子查询中尽量添加合适的索引,提升子查询本身的执行效率
- 如果子查询结果集较大,可以考虑通过业务逻辑拆分查询,避免物化产生磁盘临时表
- 使用EXPLAIN命令查看执行计划,确认子查询是否被优化器正确处理,是否存在未物化或者未重写的低效场景
可以通过EXPLAIN查看子查询的处理方式,例如执行计划中出现Materialized字段就表示子查询被物化:
EXPLAIN SELECT * FROM t1 WHERE id IN (SELECT t1_id FROM t2 WHERE status = 1);
注意:不同MySQL版本对子查询的优化策略存在差异,建议结合实际使用的版本进行测试验证。