关联子查询性能差的根本原因是嵌套循环执行模型:主表每行触发一次完整子查询,100万行即100万次解析、计划生成与计算;DEPENDENT SUBQUERY是明确信号,表明优化器放弃物化或下推。

关联子查询在数据量增大时性能急剧下降,根本原因不是“写法不优雅”,而是执行模型被数据库强制锁定为嵌套循环:主表每行都触发一次完整子查询执行,100 万行 = 100 万次独立解析、计划生成、索引查找、聚合计算。
DEPENDENT SUBQUERY 是性能崩塌的明确信号
只要 EXPLAIN 输出中出现 DEPENDENT SUBQUERY 或 SubPlan 节点,就说明优化器已放弃物化或下推——它无法把子查询提前算好,只能老老实实跟着主表一行一行跑。
- 看
rows列:若显示扫描 50 万行但最终只返回 300 行,基本确认是嵌套循环放大 - 看
type:如果是ALL或index,且Extra含Using where; Using temporary,说明连索引都没走稳 - 真实耗时里子查询部分占 85% 以上,
EXPLAIN ANALYZE会直接暴露这个瓶颈
标量子查询必须提前聚合,不能留着“每行触发”
像 SELECT id, (SELECT COUNT(*) FROM logs WHERE user_id = u.id) FROM users u 这类写法,表面简洁,实际是让数据库重复做同一件事一百万次。哪怕 logs(user_id) 有索引,也只是加速单次扫描,无法减少调用次数本身。
- 正确做法是先聚合:
SELECT user_id, COUNT(*) AS cnt FROM logs GROUP BY user_id,再LEFT JOIN回主表 - 如果日志表超大(比如上亿行),聚合前必须加时间过滤,例如
WHERE create_time > DATE_SUB(NOW(), INTERVAL 7 DAY) - 别在聚合后的派生表里再引用主表字段做条件——这又变回相关子查询,前功尽弃
JOIN 改写不是无脑替换,三类逻辑陷阱最易踩
把 IN 或标量子查询改成 JOIN 后速度飙升,但结果可能已经错了——很多线上 bug 就出在这一步。
-
NOT IN子查询含 NULL 时永远返回空,而LEFT JOIN ... IS NULL必须显式加AND right.id IS NOT NULL才等价 - 原标量子查询返回 2 行会立刻报错
more than one row returned;JOIN 后静默产生笛卡尔积,污染结果却难察觉 -
AVG()和COUNT()在空组行为不同:COUNT()返回 0,AVG()返回 NULL,混用时改写稍有偏差,统计口径就全偏了
真正难的不是怎么写 JOIN,而是判断哪一层括号在执行时会触发重复计算——很多 SQL 看似结构清晰,实则藏着 O(N×M) 的扫描。改完之后,务必用 EXPLAIN 对比 rows 和 Extra,而不是只看执行时间;缓存会让时间失真,但扫描行数不会说谎。


















