无关子查询执行一次,相关子查询可能每行外层数据都执行一遍;判断依据是子查询是否依赖外层字段,EXPLAIN中select_type为DEPENDENT SUBQUERY即为相关。

子查询到底执行几次?先看它是相关还是无关
无关子查询只执行一次,相关子查询可能每行外层数据都执行一遍——这是性能分水岭。比如 SELECT * FROM Product WHERE approvalStatus = (SELECT pk FROM ArticleApprovalStatus WHERE code = 'approved'),括号里不依赖外层字段,属于无关子查询,查一次就够了;但换成 SELECT * FROM Product p WHERE EXISTS (SELECT 1 FROM Order o WHERE o.productId = p.pk),o.productId = p.pk 中的 p.pk 来自外层,就变成相关子查询,MySQL 可能为每一行 Product 都跑一遍内层查询。
- 判断方法:把子查询单独复制出来执行,能跑通就是无关;报错(比如提示列不存在)大概率是相关
- EXPLAIN 看
select_type字段:SUBQUERY通常无关,DEPENDENT SUBQUERY就是相关 - 相关子查询在百万级表上极易拖垮响应,别盲目套用“逻辑清晰”的写法
IN 和 EXISTS 谁更快?取决于子查询结果大小和索引
IN 和 EXISTS 表面功能相似,底层策略完全不同:IN 常触发物化(Materialization),把子查询结果建临时表再做哈希查找;EXISTS 更倾向走半连接(semi-join)或反向索引查找。但实际谁快,得看数据分布。
- 子查询结果小(IN 物化后 lookup 很快,可接受
- 子查询结果大(>1000 行)或无索引:
EXISTS更稳,避免建大临时表;但若外层表没索引匹配字段,也会退化成嵌套循环 - 特别注意:
NOT IN遇到 NULL 会整个返回空集,而NOT EXISTS不受 NULL 影响——这是语义坑,不是性能坑
MySQL 怎么“物化”子查询?临时表不是白建的
MySQL 没有独立“物化引擎”,但优化器会在判定子查询不可合并(non-mergeable)时,主动把它执行一遍,结果存进内存或磁盘临时表,再让外层查询去查这张表。这个过程叫 Materialization,它省了重复计算,但代价是建表 + 查表两步开销。
- 触发条件常见于:
IN后跟聚合子查询(如SELECT id FROM t1 WHERE x IN (SELECT MAX(y) FROM t2 GROUP BY z)),或子查询含GROUP BY/DISTINCT - 临时表默认用 MEMORY 引擎,但超出
tmp_table_size会自动转成 MyISAM 或 InnoDB 磁盘表,I/O 成倍增加 - 用
EXPLAIN FORMAT=JSON查看materialized_from_subquery字段,能确认是否走了物化路径
SQLite 的“拍扁”和 MySQL 的“合并”不是一回事
SQLite 的 Subquery Flattening 是激进优化:直接把子查询逻辑下推进外层 WHERE,删掉嵌套结构,变成单层扫描。比如 SELECT a FROM (SELECT x+y AS a FROM t1 WHERE z5 会被拍成 SELECT x+y AS a FROM t1 WHERE z5。MySQL 也支持类似优化(称为 subquery merging),但更保守,只对简单无关子查询生效,且要求子查询不含聚合、窗口函数、LIMIT 等限制项。
- 想让 MySQL 尽量合并?子查询尽量只含
SELECT+FROM+WHERE,别加ORDER BY、GROUP BY、HAVING - 拍扁后能用上索引,合并后能减少嵌套层级——但两者都失败时,你就得手动重写成 JOIN
- 别假设所有数据库都“懂你”,PostgreSQL 默认不物化,Oracle 可能走 FILTER,执行策略差异比语法差异更值得盯紧
真正卡住人的从来不是语法会不会写,而是不知道某一行 SQL 在自己用的数据库里,到底被拆成了几步、建了几个临时结构、扫了几遍磁盘。查 EXPLAIN 不是仪式,是读执行现场的唯一方式。

















