ROW_NUMBER()在分组多、N小时特别慢,因其需全表扫描、排序并编号后才过滤,而LATERAL/CROSS APPLY可按需每组取前N行,避免冗余IO与计算。

分组 Top N 查询慢,八成不是写法问题,而是没让数据库“少读数据”——ROW_NUMBER() 会扫全表打序号再过滤,LATERAL 或 CROSS APPLY 才能真正按需取数。
为什么 ROW_NUMBER() 在分组多、N 小时特别慢?
它必须先给每行算出序号,哪怕你只要每组前 2 条。比如 10 万员工、500 个部门,ROW_NUMBER() 会扫描全部 10 万行、排序、编号,再 WHERE 过滤;而实际只需读最多 1000 行(500 × 2)。
-
ROW_NUMBER()的执行顺序:全表扫描 → 内存/磁盘排序 → 编号 → 过滤,IO 和 CPU 开销都高 - 即使加了索引,如果
PARTITION BY和ORDER BY字段没建对联合索引(如(dept_id, salary)),仍会触发Using filesort - MySQL 8.0+ 执行计划里看到
WindowAgg下挂Sort节点,基本等于在排序上卡住
什么时候该换用 LATERAL / CROSS APPLY?
当你只关心每组聚合结果(比如平均薪资、最高分),且 N ≤ 5、分组数 ≥ 几十,LATERAL(PostgreSQL/MySQL 8.0+)或 CROSS APPLY(SQL Server)能跳过绝大多数数据。
- PostgreSQL 示例:
LATERAL (SELECT salary FROM employees e WHERE e.dept_id = d.dept_id ORDER BY salary DESC LIMIT 2)—— 每个部门只查最多 2 行 - SQL Server 必须用
TOP 2,不能用OFFSET FETCH;CROSS APPLY子查询里ORDER BY缺失会导致结果不可复现 - MySQL 8.0+ 需确认优化器启用:
SET optimizer_switch='derived_merge=off';,否则LATERAL可能被重写失效 - 漏写
LATERAL关键字,或外层表没加别名(如写departments.dept_id而非d.dept_id),PostgreSQL 直接报invalid reference to FROM-clause entry
哪些场景反而该坚持用 ROW_NUMBER()?
不是所有 Top N 都适合 LATERAL。当你要返回完整行字段(姓名、ID、图片 URL)、N 较大(Top-20+)、或分组数少但单组数据量极大时,ROW_NUMBER() 更稳。
- 要同时显示排名和累计占比:
SUM(sales) OVER (ORDER BY rn ROWS UNBOUNDED PRECEDING)必须依赖窗口函数链式计算 - 导出每个品类销量前 10 的商品明细(含 8+ 字段),
LATERAL展开后 JOIN 行数爆炸,JOIN 成本远超一次扫描 - MySQL 5.7 或更老版本不支持窗口函数,硬上
LATERAL会报错:FUNCTION xxx.LATERAL does not exist - 分组键本身来自复杂子查询(如
SELECT DISTINCT dept_id FROM active_depts),LATERAL外层结果不全,对应分组直接消失
最易被忽略的点是索引设计:无论选哪种方案,(group_col, sort_col) 复合索引都是前提。没它,LATERAL 也会退化成每组全表扫描,ROW_NUMBER() 则必然 Using filesort。别调优 SQL 前先看执行计划里有没有 Index Seek 或 Index Scan。


















