相关子查询慢的本质是外层每行都重复执行内层逻辑,10万行即执行10万次;explain中type=dependent subquery是硬信号,必须改写而非仅调索引。

相关子查询慢,本质是数据库为外层每一行重复执行内层逻辑——10万行主表就等于执行10万次子查询。看到 EXPLAIN 输出中 type = DEPENDENT SUBQUERY,必须立刻改写,不能只调索引。
确认是否真是相关子查询
先用 EXPLAIN 看执行计划,重点盯三处:
-
type列是否为DEPENDENT SUBQUERY—— 这是最硬的信号,说明子查询引用了外层字段(如WHERE user_id = users.id) -
rows列数值是否远超实际返回行数(比如扫描 50 万行,只返回 300 行),这是嵌套循环放大的铁证 -
Extra是否含Using temporary; Using filesort,说明没走索引、还建了临时表
如果子查询里有 ORDER BY + LIMIT 1 或 COUNT(*),哪怕没显式写外层引用,也可能被优化器判定为依赖型——因为聚合或排序结果无法提前物化。
IN/NOT IN 子查询改 LEFT JOIN 的三个硬条件
不是所有 IN 都能直接换 JOIN,漏掉任一条件就会语义错误或性能更差:
- 子查询结果含
NULL时,NOT IN会永远返回空——必须用LEFT JOIN ... ON ... WHERE right.id IS NULL AND right.id IS NOT NULL显式排除NULL - 主表和子表是一对多关系(如一个用户有多条日志),
INNER JOIN会导致主表行数膨胀——要么加DISTINCT,要么改用EXISTS - 子查询带
ORDER BY + LIMIT 1(如取每个用户的最新订单),不能直接JOIN——得先用派生表或窗口函数聚合,再关联
示例:原查询 SELECT id FROM users WHERE id NOT IN (SELECT user_id FROM logs WHERE status = 'error'),正确改写为:
SELECT u.id FROM users u<br>LEFT JOIN (SELECT DISTINCT user_id FROM logs WHERE status = 'error') l ON u.id = l.user_id<br>WHERE l.user_id IS NULL;
标量子查询必须提前聚合
像 SELECT id, (SELECT COUNT(*) FROM logs WHERE user_id = u.id) AS cnt FROM users u 这种,每行都触发一次全表扫描+聚合,开销爆炸:
- 正确做法是把子查询提前算好:
SELECT user_id, COUNT(*) AS cnt FROM logs GROUP BY user_id,再LEFT JOIN回主表 - 如果日志表上亿行,聚合前务必加时间范围过滤,比如
WHERE create_time > DATE_SUB(NOW(), INTERVAL 7 DAY) - 避免在子查询的
WHERE中再次引用主表字段做条件(如AND u.status = 'active'),这又变回相关子查询
聚合结果量大时,GROUP BY 字段必须有索引;若无,先建 (user_id, create_time) 联合索引。
别信“版本新就自动优化”
MySQL 8.0+ 默认启用半连接(Semi-Join)优化,但前提是子查询满足条件:不带 ORDER BY、不引用外层字段、结果集可物化。现实中,只要子查询里出现 users.id 这类引用,优化器就放弃半连接,退回到嵌套循环。
真正难的不是怎么改写,而是判断哪一层括号在执行时会触发重复计算——很多 SQL 看似简洁,实则藏着指数级扫描。改完之后,务必用 EXPLAIN 对比 rows 和 Extra 字段,而不是只看执行时间。时间可能受缓存干扰,但扫描行数不会说谎。










