相关子查询会为外层每行重复执行,识别标志是子查询中引用外层表列(如o.id);优化方案包括LEFT JOIN预聚合、窗口函数(MySQL 8.0+)及合理选用EXISTS/NOT EXISTS。

生产环境里查着查着就超时,八成是相关子查询在背后反复执行——它不是慢,是“每行都重算一遍”,10 万行订单,子查询就跑 10 万次。
怎么一眼认出相关子查询
关键看子查询里有没有引用外层表的列。只要出现 e.department_id、o.user_id 这类带外层别名的字段,就是相关子查询。
- 非相关子查询:只运行一次,比如
(SELECT AVG(salary) FROM employees) - 相关子查询:每行外层结果都触发一次,比如
(SELECT COUNT(*) FROM logs l WHERE l.order_id = o.id) - EXPLAIN 看不到子查询执行次数,但
rows值会异常高,且Extra里常带Using temporary或Using filesort
用 LEFT JOIN 替换最稳妥
把子查询逻辑提前聚合,再关联,避免重复计算。MySQL 5.7+、8.0 都能稳定走索引。
- 原写法(危险):
SELECT id, (SELECT COUNT(*) FROM logs l WHERE l.order_id = o.id) cnt FROM orders o - 改写后(推荐):
SELECT o.id, COALESCE(l.cnt, 0) cnt FROM orders o LEFT JOIN (SELECT order_id, COUNT(*) cnt FROM logs GROUP BY order_id) l ON o.id = l.order_id - 注意:
logs.order_id必须有索引,否则子查询聚合本身也慢 - 如果外层
orders本身带WHERE条件(如WHERE status = 'shipped'),建议先用 CTE 或临时表收窄驱动集,再 JOIN
用窗口函数替代(MySQL 8.0+/PostgreSQL)
适合需要按组统计又不想改结构的场景,语义清晰、执行一次到位。
- 原写法:
SELECT name, salary > (SELECT AVG(salary) FROM employees e2 WHERE e2.dept = e1.dept) is_above_avg FROM employees e1 - 改写后:
SELECT name, salary > AVG(salary) OVER (PARTITION BY dept) is_above_avg FROM employees - 优势:不额外扫描、不生成临时表、无 JOIN 开销
- 限制:不支持 MySQL 5.7 及更早版本;PARTITION BY 字段必须有索引才能高效分组
IN/EXISTS 别乱换,得看数据分布
不是所有子查询都要转 JOIN。简单存在性判断,EXISTS 往往比 IN 更快,尤其当子查询结果集大、外层表小时。
- 外层小、子查询大 → 优先
EXISTS:SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid') - 外层大、子查询小 → 可考虑
IN,但必须确保子查询字段有索引:SELECT * FROM orders WHERE user_id IN (SELECT id FROM vip_users) -
NOT IN有陷阱:子查询结果含NULL会导致整个条件为UNKNOWN,结果为空——一律改用NOT EXISTS
最容易被忽略的是:相关子查询一旦嵌套两层以上(比如子查询里还套子查询),优化器基本放弃推导,直接退化成暴力循环。这种时候别硬调,拆成中间表或应用层预聚合更实际。

















