窗口函数排序必须全量加载分区数据,因其执行阶段在where/jon之后、结果返回之前,需先获取完整中间结果集再按partition by和order by分区排序——即使只取row_number()=1,也须载入整个分区所有行排序。

窗口函数排序必须全量加载分区数据
窗口函数不是边读边排,而是先把整个 PARTITION BY 分区的所有行一次性读进内存,再按 ORDER BY 排序——哪怕你只取 ROW_NUMBER() = 1,数据库也得把该分区全部数据拉进来排好。这不是优化器没尽力,是 SQL 标准规定的执行模型:窗口计算发生在 WHERE/JOIN 之后、最终结果返回之前,中间结果集必须完整。
没索引 + 大分区 = Sort Method: external merge Disk
PostgreSQL 的 EXPLAIN (ANALYZE) 里一旦看到 WindowAgg 节点下挂的是 Seq Scan 或未命中索引的 Index Scan,且 Actual Rows 动辄几十万,同时出现 Sort Method: external merge Disk,就是内存已溢出的铁证。MySQL 则看 Extra 是否含 Using filesort,再配合 SHOW STATUS LIKE 'Sort_merge_passes' 持续上涨确认。
- 复合索引顺序必须严格匹配:比如
PARTITION BY user_id ORDER BY created_at DESC,索引就得建为(user_id, created_at),反过来或缺字段都无效 -
WHERE条件不能写在窗口外就以为能下推——它对窗口输入无影响,必须提前塞进子查询或 CTE - 分区键离散度高(如上万不同
user_id)时,每个分区单独排序,内存压力是乘数级的
work_mem 不够会直接落盘,但调太大有并发风险
PostgreSQL 窗口排序走的就是 work_mem 控制的内存路径。单个分区排序所需内存 ≈ 行数 × 排序键大小 × 1.5,比如 10 万行、每行排序键 24 字节,至少需要约 4.8MB。若实际 sort_bytes(查 pg_stat_progress_sort)远超当前 work_mem,就必然落盘。
- 先用
SET LOCAL work_mem = '16MB'测,再逐步试'32MB'、'64MB' - 超过
'128MB'要警惕:一条含多个窗口函数的查询可能并发启动 3–5 个排序,10 个并发就可能吃掉 1GB+ 物理内存 -
work_mem对窗口函数无效,别指望靠它缓解
真正难处理的是跨大分区的累计计算
像用户全生命周期流水求 SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at) 这类场景,光靠索引和过滤拦不住数据量——分区本身就有几万行,排序无法避免。这时候得结合业务妥协:接受近似结果、按月拆分时间粒度、或改用物化中间表预计算。
最容易被忽略的一点:窗口函数慢,90% 不是 SQL 写得不好,而是没意识到它根本不会“跳过”数据——只要输入集没提前拦住,后面所有优化都是徒劳。











