相关子查询每次外层行都重新执行,因其引用外层列(如t1.id),数据库只能在每行代入后执行,形成嵌套循环;外层N行则执行N次,性能随数据量线性下降,宜用窗口函数或自连接优化。

相关子查询为什么每次外层行都重新执行
因为相关子查询里引用了外层查询的列(比如 t1.id),数据库无法提前算出结果,只能在外层每取出一行时,把该行的值代入子查询再执行一次。本质是“嵌套循环”语义——外层 N 行,子查询就可能跑 N 次。
常见错误现象:EXPLAIN 显示子查询类型为 DEPENDENT SUBQUERY,且执行时间随外层数据量线性增长;在 MySQL 5.7 或早期版本中尤其明显;PostgreSQL 则可能显示 SubPlan 并标注 rows=1 多次。
- 使用场景:查每个用户的最新订单(
WHERE order_time = (SELECT MAX(...) FROM orders o2 WHERE o2.user_id = u.id)) - 性能影响:外层 10 万行 → 子查询执行 10 万次,哪怕加了索引,I/O 和解析开销也剧增
- 容易踩的坑:误以为加了
user_id索引就能避免重复执行——索引只加速单次子查询,不改变执行次数
用 JOIN + 窗口函数替代(推荐,MySQL 8.0+/PostgreSQL/SQL Server)
窗口函数能一次性给每组数据打上序号或聚合值,避免逐行回查。核心思路是:先算出所有需要的聚合/排名信息,再和主表关联。
例如找每个用户的最新订单:
SELECT u.name, o.order_no, o.order_time
FROM users u
JOIN (
SELECT user_id, order_no, order_time,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn
FROM orders
) o ON u.id = o.user_id AND o.rn = 1;
-
ROW_NUMBER()比MAX()更可控,能处理并列情况(如需保留全部最新可改用RANK()) - 必须写
o.rn = 1在ON或WHERE中,否则会返回冗余行 - 注意:若
orders表没建(user_id, order_time)联合索引,PARTITION BY + ORDER BY仍可能慢
用 LEFT JOIN 自连接排除法(兼容老版本 MySQL 5.6)
原理是:如果不存在“时间更大”的同用户订单,那当前订单就是最新的。用 LEFT JOIN 找不到匹配项的行即为目标。
等价改写:
SELECT u.name, o1.order_no, o1.order_time FROM users u INNER JOIN orders o1 ON u.id = o1.user_id LEFT JOIN orders o2 ON o1.user_id = o2.user_id AND o1.order_time < o2.order_time WHERE o2.user_id IS NULL;
- 关键点:
o1.order_time 是严格小于,不是 <li>必须给 <code>orders(user_id, order_time)加联合索引,否则LEFT JOIN变全表扫描 - 缺点:语义绕、可读性差,且当存在 NULL 时间时逻辑易错(建议先
WHERE order_time IS NOT NULL)
什么时候不该硬改?留着相关子查询反而更稳
不是所有相关子查询都该消灭。如果外层结果集极小(比如只查 3 个指定 ID 的用户),或子查询本身极轻量(只查单行主键),强行 JOIN 可能增加计划复杂度甚至更慢。
- 典型安全场景:在
WHERE id IN (1, 5, 9)后跟相关子查询;或子查询是EXISTS (SELECT 1 FROM config WHERE key = t1.type)这类有强索引的小表探测 - 验证方法:对实际数据
EXPLAIN FORMAT=JSON对比两种写法的rows_examined和execution_time - 最容易被忽略的点:改写后没重写索引——新查询路径依赖的索引和原相关子查询不同,不加索引可能从“慢但可控”变成“慢到超时”

















