相关子查询慢是因为被外层表每行重复执行,如5万行触发5万次;应改用join+聚合、exists替代in、标量子查询需谨慎并确保索引可用。

相关子查询为什么慢?先看执行次数
相关子查询慢,根本原因是它被重复执行——外层表每行都触发一次子查询。比如 SELECT name FROM emp WHERE salary > (SELECT AVG(salary) FROM emp WHERE dept_id = emp.dept_id),如果 emp 有 5 万行,这个子查询就执行 5 万次,而不是 1 次。
关键判断点就一个:WHERE 或 SELECT 里的子查询中是否出现外层表的列(如 emp.dept_id)。只要出现,就是相关子查询,性能风险立即上升。
注意:数据库优化器几乎不会自动把这类子查询“拉出来”重写,它默认按语义逐行执行。
用 JOIN + 聚合替代 WHERE 中的相关子查询
这是最常用、效果最直接的改写方式。核心是把“每行算一次”变成“先算好再关联”。
- 原写法:
SELECT name, salary FROM emp e WHERE salary > (SELECT AVG(salary) FROM emp WHERE dept_id = e.dept_id) - 改写后:
SELECT e.name, e.salary FROM emp e JOIN (SELECT dept_id, AVG(salary) avg_sal FROM emp GROUP BY dept_id) dept_avg ON e.dept_id = dept_avg.dept_id WHERE e.salary > dept_avg.avg_sal
好处是显而易见的:
- 子查询只执行 1 次,聚合结果集小,JOIN 效率高
- 数据库能对
dept_avg的dept_id做哈希连接或索引查找 - 避免了 N+1 扫描,执行计划里看不到
Dependent subquery类型节点
别忘了给 emp(dept_id, salary) 加联合索引,否则内层 GROUP BY 仍可能慢。
用 EXISTS 替代 IN 处理存在性判断
当相关子查询只用来判断“是否存在”,比如 WHERE id IN (SELECT user_id FROM order WHERE status = 'paid' AND user_id = u.id),这本质是相关子查询,且极易出错。
问题不止于性能:
-
IN遇到子查询返回NULL时,整行被过滤(SQL 三值逻辑) - 优化器对
IN常做全量物化,内存占用高 - 无法利用
user_id上的索引下推过滤
换成 EXISTS:
WHERE EXISTS (SELECT 1 FROM order o WHERE o.user_id = u.id AND o.status = 'paid')
优势:
- 语义清晰:“只要找到一条就停”,多数引擎会走半连接(semi-join)
- 不受
NULL影响,行为可预期 - 能下推
o.user_id = u.id条件,索引可用性更高
标量子查询在 SELECT 列表里要格外小心
SELECT id, (SELECT COUNT(*) FROM log WHERE user_id = u.id) cnt FROM user u 这类写法,看着简洁,但实际是性能地雷。
它强制数据库用嵌套循环执行:对 u 每一行,都单独跑一次子查询。即使 log(user_id) 有索引,50 万用户 × 单次索引查找 = 50 万次随机 I/O。
更稳的解法:
- 先聚合:
WITH user_log_cnt AS (SELECT user_id, COUNT(*) cnt FROM log GROUP BY user_id),再LEFT JOIN - 确认业务是否真需要实时值——很多场景用每日快照表或冗余字段更可靠
- 如果必须用标量子查询,确保子查询 WHERE 条件能走索引,且返回行数极少(最好常驻内存)
真正容易被忽略的是:这种写法在测试库数据少时完全不暴露问题,一上生产就崩。压测时一定要用接近真实量级的数据验证。











