窗口函数慢大概率是work_mem不足,因其分区排序依赖work_mem,超限会落盘导致性能骤降;应通过EXPLAIN或pg_stat_progress_sort确认落盘,再按需调大work_mem。

窗口函数慢,大概率是 work_mem 不够用
PostgreSQL 窗口函数(如 ROW_NUMBER()、RANK()、SUM() OVER (PARTITION BY ... ORDER BY ...))执行时,内部必须对每个 PARTITION BY 分区做排序。这个排序走的就是 work_mem 控制的内存路径——一旦数据量超过 work_mem 阈值,就会触发 external merge Disk,性能断崖式下跌。
别急着重写 SQL 或加索引。先确认是不是内存瓶颈:运行 EXPLAIN (ANALYZE, BUFFERS),重点看执行计划里有没有 WindowAgg 或 Sort 节点带 Sort Method: external merge Disk;或者查 pg_stat_progress_sort.sort_bytes 是否远超当前 work_mem 设置值。
- 只要看到
Disk字样或sort_bytes > work_mem,就说明窗口排序已落盘,调大work_mem是最直接有效的解法 - 注意:窗口函数的排序和
GROUP BY的哈希聚合不同,它不走HashAggregate,所以pg_stat_progress_hash对它无效 - 如果分区数多(比如
PARTITION BY user_id有上万不同值),每个分区都要独立排序,内存压力是乘数级的
怎么设 work_mem 才不翻车
work_mem 不是越大越好,尤其对窗口函数这种可能触发多路并行排序的操作。一个含 ORDER BY + 多个窗口函数的查询,可能同时启动 3–5 个独立排序操作,每份都吃掉一份 work_mem。
- 起始建议从
SET LOCAL work_mem = '16MB'开始测;若仍落盘,逐步试'32MB'、'64MB' - 超过
'128MB'要谨慎:单条查询理论峰值内存 ≈ 排序路数 ×work_mem,并发 10 个这类查询就可能吃掉 1GB+ 物理内存,触发 swap - 估算下限可参考:假设单个分区平均 10 万行,每行排序键 24 字节,加上指针和开销,粗略需内存 ≈ 100000 × 32 × 1.5 ≈ 4.8MB —— 这就是该分区排序的保底
work_mem - 别改全局
postgresql.conf:窗口函数慢通常只出现在报表类查询,全局调高会污染所有 OLTP 查询,得不偿失
在应用里安全地临时调高 work_mem
最稳妥的方式是在业务逻辑中对目标查询显式设置会话级 work_mem,事务结束自动还原,不影响其他请求。
- 在应用代码里执行查询前加一句:
BEGIN; SET LOCAL work_mem = '64MB'; SELECT ... OVER (...); COMMIT; - 如果用连接池(如 PgBouncer),确认它没禁用
SET命令——某些池子默认过滤运行时参数修改,需配置ignore_startup_parameters = work_mem - 避免在函数体内用
SET LOCAL影响调用者事务;若必须封装,函数需声明为VOLATILE并显式开启事务 - 不要用
SET work_mem = '64MB'(无LOCAL):它会污染整个会话,后续任意查询都继承该值,风险不可控
别碰 shared_buffers 和 effective_cache_size 来“救”窗口函数
这两个参数和窗口函数的排序性能完全无关:shared_buffers 缓存的是数据页,effective_cache_size 只影响优化器选执行计划,它们都不参与窗口函数的中间排序过程。
真正在窗口函数里起作用的只有 work_mem。调错地方不仅白忙活,还可能干扰缓存命中率(比如盲目加大 shared_buffers 挤占系统可用内存,反而让 work_mem 更容易触发 swap)。
复杂点在于:窗口函数的排序开销是隐式的、分散的,不像 ORDER BY 那么直观;容易被误判成“SQL 写得差”或“缺索引”,其实只是内存没给够。










