关联子查询易致全表扫描,因每行外层记录均需执行一次子查询;改用ROW_NUMBER()窗口函数配合LEFT JOIN可一次性分组排序,避免重复扫描。

为什么关联子查询容易导致全表扫描
关联子查询(correlated subquery)在 WHERE 或 SELECT 中引用外部表字段时,数据库往往无法提前估算结果集大小,被迫对每行外部记录都执行一次子查询——相当于嵌套循环。比如:
SELECT a.id, a.name, (SELECT MAX(b.created_at) FROM orders b WHERE b.user_id = a.id) AS last_order_time FROM users a;这里每查一个
a.id,都要扫一遍 orders 表中匹配的行,orders 若无合适索引或数据量大,I/O 和 CPU 开销会指数级上升。
用 ROW_NUMBER() + LEFT JOIN 替代最常见场景
当子查询是取“每个用户最新/最早一条订单”这类 Top-N 每组需求时,窗口函数能一次性完成分组排序,避免重复扫描。
SELECT a.id, a.name, b.last_order_time FROM users a LEFT JOIN ( SELECT user_id, created_at AS last_order_time, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders ) b ON a.id = b.user_id AND b.rn = 1;
- 必须加
PARTITION BY user_id,否则所有订单被当成一组排序 -
ORDER BY created_at DESC决定“最新”的定义,别漏DESC -
LEFT JOIN保证没订单的用户不丢行,若用INNER JOIN会过滤掉零订单用户 - 子查询里不能写
WHERE rn = 1,因为窗口函数在WHERE之后执行,必须用ON或外层WHERE过滤
MAX() / MIN() 窗口聚合比子查询更轻量
如果子查询只求聚合值(如每个用户的订单总数、最大金额),直接用聚合窗口函数即可,无需 JOIN:
SELECT id, name, COUNT(*) OVER (PARTITION BY id) AS order_count FROM users u JOIN orders o ON u.id = o.user_id;但注意:
- 这会把每个用户的所有订单行都展开,结果行数 = 订单数,不是用户数
- 若只要一行用户 + 聚合值,得用
GROUP BY配合普通聚合;窗口函数适合“保留明细行同时附带聚合信息”的场景 -
COUNT(*) OVER (...)不等价于(SELECT COUNT(*) FROM orders WHERE user_id = u.id)—— 前者统计当前连接后的分组行数,后者只统计orders表中匹配行,语义一致的前提是JOIN无重复或丢失
改写后仍慢?检查执行计划里的两个关键点
窗口函数不是银弹。即使语法改写正确,性能也可能没改善,重点看:
-
EXPLAIN是否出现WindowAgg节点,且其Rows Removed by Filter很高?说明PARTITION BY字段缺乏索引,排序成本大 -
Sort节点是否在磁盘上执行(Sort Method: external merge Disk: XXXkB)?意味着内存不足,需调大work_mem或优化分区粒度 - 原关联子查询如果有
orders表上的(user_id, created_at)复合索引,改写后这个索引依然有效——但窗口函数不会自动利用它做排序,仍可能走全表排序
PARTITION BY 字段局部有序,否则排序开销会吃掉收益。

















