相关子查询慢的本质是外层每行触发内层重复执行,导致嵌套循环放大开销;需通过explain识别dependent subquery,用join替代时须满足无null、一对多处理及聚合提前等约束。

相关子查询慢,本质是数据库被迫为外层每一行重复执行一次内层逻辑——不是它写得复杂,而是执行模型被拖垮了。
EXPLAIN 看到 DEPENDENT SUBQUERY 就该停手
只要子查询的 WHERE、ON 或标量表达式里引用了外层表字段(比如 b.user_id = a.id),MySQL/PostgreSQL 就会标记为 DEPENDENT SUBQUERY。这不是警告,是明确信号:优化器已放弃预计算,退化为嵌套循环。
- 外层 10 万行 → 子查询执行 10 万次,哪怕每次只走索引,I/O 和 CPU 开销也线性放大
-
EXPLAIN中rows列若远超实际结果数(如扫描 50 万行只返回 300 行),基本确认是嵌套循环放大 -
type为ALL或index,且Extra含Using temporary或Using filesort,说明没走索引、还建了临时表
IN / EXISTS 改 JOIN 不是无脑替换
直接把 WHERE id IN (SELECT ...) 换成 INNER JOIN 很容易出错,必须满足三个硬约束:
- 子查询结果不含
NULL:否则NOT IN会永远返回空,得用LEFT JOIN ... WHERE right.id IS NULL,并显式加AND right.id IS NOT NULL - 主表和子表是一对多关系(如用户→订单):直接
INNER JOIN会导致主表行数膨胀,要么加DISTINCT,要么改用EXISTS - 子查询带
ORDER BY + LIMIT 1(如取每个用户的最新订单):不能直接 JOIN,得先用派生表或窗口函数聚合,再关联
标量子查询必须提前聚合
像 SELECT id, (SELECT COUNT(*) FROM logs WHERE user_id = u.id) FROM users u 这种写法,本质是“每行触发一次全表 COUNT”,开销极大。
- 正确做法是把聚合提前:先
SELECT user_id, COUNT(*) AS cnt FROM logs GROUP BY user_id,再LEFT JOIN回主表 - 如果日志表上亿行,聚合前务必加时间过滤,例如
WHERE create_time > DATE_SUB(NOW(), INTERVAL 7 DAY) - 避免在子查询里用
WHERE引用主表字段做条件过滤——这又变回相关子查询
最容易被忽略的坑:执行时机不等于代码顺序
很多 SQL 看似只有一层括号,实则藏着指数级扫描。真正难的不是怎么改写,而是判断哪一层在执行时会触发重复计算。改完之后,务必用 EXPLAIN 对比 rows 和 Extra 字段,而不是只看执行时间——缓存可能让“快”变成假象。











