窗口函数必须全量排序,因其执行阶段在WHERE/JON之后、结果返回之前,需先获取完整中间结果集再按PARTITION BY和ORDER BY分区排序——即使只取ROW_NUMBER()=1,也须载入整个分区数据排序。

窗口函数执行时为何必须全量排序
因为窗口函数的执行阶段在 WHERE / JOIN 之后、最终结果返回之前,数据库必须先拿到完整中间结果集,再按 OVER 子句指定的 PARTITION BY 和 ORDER BY 做分区内部排序——哪怕你只取 ROW_NUMBER() = 1,它也得把整个分区所有行读进来排好序。这不是优化器偷懒,是 SQL 标准定义的执行模型。
EXPLAIN 中看到 WindowAgg 就该警惕
PostgreSQL 的 EXPLAIN (ANALYZE) 显示 WindowAgg 节点在最外层,且其子节点是 Seq Scan 或未命中索引的 Index Scan,基本等于确认:数据没过滤干净、排序字段无有效索引、窗口范围过大。此时 Sort Method: external merge Disk 就是溢出已发生的铁证;MySQL 则看 Extra 是否含 Using filesort,再结合 SHOW STATUS LIKE 'Sort_merge_passes' 持续上升来交叉验证。
WHERE 必须写在窗口函数外面,不能指望下推
- 窗口函数本身不支持条件下推,
WHERE status = 'shipped' ORDER BY created_at写在窗口内或外,对执行计划毫无影响 - 正确做法是用子查询或 CTE 先过滤:
SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC) FROM (SELECT id, created_at FROM orders WHERE status = 'shipped') t - 这个子查询若能走
(status, created_at)复合索引,就能避免全表扫描,直接 range 扫描 + 索引有序返回,彻底绕过内存排序
别用 ROW_NUMBER() 实现分页,除非真要全局序号
90% 的「查最新 N 条」场景,根本不需要 ROW_NUMBER()。直接 ORDER BY created_at DESC LIMIT 100,只要 created_at 有索引,就是毫秒级;而 ROW_NUMBER() OVER (ORDER BY created_at DESC) 在千万级表上会强制加载全部匹配行进内存排序——这是设计误用,不是性能调优问题。
真正难处理的,是那些必须跨大分区做累计计算(比如用户全生命周期流水求 running_total)的场景。这时候光靠索引和过滤不够,得结合业务接受近似结果、拆分时间粒度、或改用物化中间表,否则内存压力不会消失。

















