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

窗口函数强制全量排序,中间结果集撑爆内存
窗口函数不是“边读边排”,而是必须等 WHERE / JOIN 阶段输出完整中间结果集后,再按 PARTITION BY 和 ORDER BY 做分区内部全量排序——哪怕你只取 ROW_NUMBER() = 1,数据库也得把整个分区所有行加载进内存排好序。这不是执行计划没优化,是 SQL 标准定义的执行模型。
一旦中间结果超几十万行,sort_buffer_size(MySQL)或 work_mem(PostgreSQL)很快耗尽,触发磁盘临时表回退。PostgreSQL 的 EXPLAIN (ANALYZE) 会直接显示 Sort Method: external merge Disk;MySQL 则看 SHOW STATUS LIKE 'Sort_merge_passes' 是否持续上升。
WHERE 条件无法下推到窗口函数内部
写成 SELECT *, ROW_NUMBER() OVER (ORDER BY created_at) FROM orders WHERE status = 'shipped' 是无效的:窗口函数不参与条件过滤,WHERE 只作用于最外层结果,但排序仍发生在过滤之后的完整中间集上。真正起效的只有把过滤提前到子查询或 CTE 中:
- 错误写法:
SELECT *, ROW_NUMBER() OVER (ORDER BY created_at) FROM orders WHERE status = 'shipped' - 正确写法:
SELECT *, ROW_NUMBER() OVER (ORDER BY created_at) FROM (SELECT id, created_at, status FROM orders WHERE status = 'shipped' AND created_at >= '2026-01-01') 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)。这时候光靠索引和过滤不够,得结合业务接受近似结果、拆分时间粒度、或改用物化中间表,否则内存压力不会消失。
ORDER BY 字段无索引,或索引顺序不匹配
即使你已用子查询过滤缩小数据集,如果 OVER (ORDER BY x, y) 中的字段没有对应索引,或索引是 (y, x) 而非 (x, y),数据库依然无法利用索引有序性,只能走 filesort。MySQL 的 EXPLAIN 若显示 type = ALL 或 key = NULL,基本就是索引没生效;PostgreSQL 的 EXPLAIN (ANALYZE) 若看到 Sort 节点无 Index Scan 支撑,也说明排序未被索引覆盖。
复合索引要严格匹配 ORDER BY 字段顺序,且前导列必须参与等值过滤(如 WHERE a = ? ORDER BY b, c → 索引应为 (a, b, c));若含范围条件(如 WHERE a > ? ORDER BY b),则仅 a 能用于索引查找,b 无法复用索引排序。











