DEPENDENT SUBQUERY 每行执行一次,导致N倍随机I/O开销;物化仅适用于非相关子查询,含外层列、GROUP BY等即失效;IN/EXISTS语义不同,改写不自动优化;JOIN更可控因执行路径明确可估算。

DEPENDENT SUBQUERY 每行都重跑一次,不是“慢”,是“乘法级开销”
只要子查询里出现外层表的列(比如 t1.id = t2.user_id),MySQL 就判定为相关子查询,执行计划里必然出现 DEPENDENT SUBQUERY。这不是“偶尔慢”,而是外层扫描 N 行,子查询就执行 N 次——10 万行订单 × 每次查用户表索引 + 回表,就是 10 万次随机 I/O。
常见错误现象:
- EXPLAIN 中
type列为ALL或range,但Extra明确写Dependent subquery - 实际执行时间随外层数据量线性甚至超线性增长(比如外层从 1 万 → 10 万行,耗时从 200ms → 3.2s)
- 即使子查询本身只返回 1 行,重复解析、权限检查、临时表初始化等开销仍不可忽略
物化(materialization)根本没生效,别信“MySQL 8.0 默认开启”
MySQL 8.0+ 确实倾向物化,但只对「非相关 + 无聚合 + 无排序 + 无 LIMIT + 无窗口函数」的子查询生效。只要子查询里带 GROUP BY、ORDER BY RAND()、LIMIT 10 或引用了外层列,优化器就直接放弃物化,退回到嵌套循环。
怎么确认物化失败?
- 用
EXPLAIN FORMAT=TREE,看到节点名是dependent subquery,且Extra含Using temporary; Using filesort - 查
optimizer_trace,搜索"materialized":false或"chosen":false在物化阶段 - 物化临时表默认不建索引:内存小用哈希(快),一落盘就用 B+ 树(比原表索引还慢)
IN 和 EXISTS 不是性能开关,是语义陷阱
把 IN (SELECT ...) 换成 EXISTS,不会自动触发物化,也不会改变执行本质——因为两者语义不同:NULL IN (1,2,NULL) 返回 UNKNOWN,而 EXISTS 只返回布尔值。优化器只在严格等价时才重写,一旦子查询含外层列或聚合,重写就被跳过。
真正影响性能的是“是否相关”,不是关键字本身:
-
WHERE id IN (SELECT user_id FROM logs WHERE logs.user_id = users.id)→ 仍是DEPENDENT SUBQUERY -
WHERE EXISTS (SELECT 1 FROM logs WHERE logs.user_id = users.id)→ 同样是DEPENDENT SUBQUERY - 想绕开?得提前抽成派生表:
FROM (SELECT DISTINCT user_id FROM logs) AS active_users,且确保 ON 条件不反向引用外层
JOIN 改写为什么更可控,而不是“更高级”
JOIN 的执行路径由驱动表、连接算法(hash join / nested loop)、索引选择共同决定,优化器能明确复用索引、下推条件、估算成本;而子查询的物化与否全靠隐式判断,稍有偏差(比如统计信息过期、tmp_table_size 设置偏小)就崩。
改写要点:
-
SELECT (SELECT name FROM users WHERE id = o.user_id)→ 改用LEFT JOIN users u ON u.id = o.user_id,保留 NULL 语义 -
WHERE id IN (SELECT user_id FROM logs)→ 优先用EXISTS,若必须 JOIN,ON里写过滤条件(如l.type = 'login'),别丢到WHERE里 - 一对多导致重复?加
DISTINCT成本高,不如子查询里先GROUP BY user_id或用派生表预聚合
最容易被忽略的,是物化是否真的发生了——不能只看版本或 optimizer_switch 设置,必须用 EXPLAIN FORMAT=TREE 或 optimizer_trace 看清优化器到底选了哪条路。



















