磁盘溢出主因是窗口函数输入数据量过大而非work_mem过小;需通过执行计划识别WindowAgg下挂全表扫描或未命中索引的扫描,结合WHERE提前过滤、建立PARTITION BY与ORDER BY匹配的复合索引,并避免大分区累计计算。

磁盘溢出不是 work_mem 设得太小,而是窗口函数喂进来太多行——只要执行计划里 WindowAgg 下挂的是全表扫描或未命中索引的 Index Scan,基本就坐实了问题。
看执行计划里有没有 WindowAgg 和 Sort 套娃
PostgreSQL 用 EXPLAIN (ANALYZE, BUFFERS),重点盯这几个信号:
-
WindowAgg节点下直接连着Seq Scan或Bitmap Heap Scan,且Actual Rows动辄百万级以上 - 紧挨着
WindowAgg的Sort节点写着Sort Method: external merge on disk -
Buffers: shared read数值远高于shared hit,说明频繁读磁盘 -
WindowAgg的width字段超过 1000 字节(比如拖了jsonb或长text字段)
分区键和排序字段没索引,窗口就只能硬扛
数据库无法“边读边算”,必须把整个分区数据加载进内存排序——哪怕你只取 ROW_NUMBER() = 1。没索引时,它得先把所有行读进来再哈希/排序分组。
-
PARTITION BY user_id ORDER BY created_at DESC→ 索引必须是(user_id, created_at),不能反过来 - MySQL 8.0 前不支持混合升/降序索引,慎用
ORDER BY a ASC, b DESC - 分区字段是表达式?比如
PARTITION BY date_trunc('month', created_at)→ 建函数索引:CREATE INDEX idx_orders_month ON orders (date_trunc('month', created_at)) - 高基数字段优先:用
user_id比用status(只有 'pending'/'done')更稳,后者容易导致 95% 数据挤进同一分区
WHERE 条件根本没下推到窗口之前
窗口函数不能被 WHERE 下推,WHERE rn = 1 这种写法会直接报错。过滤必须提前,否则白加。
- 错误写法:
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn FROM orders WHERE status = 'shipped'→ 过滤在窗口之后,无效 - 正确写法:
SELECT * FROM (SELECT id, user_id, created_at FROM orders WHERE status = 'shipped' AND created_at >= '2026-04-01') t WINDOW w AS (PARTITION BY user_id ORDER BY created_at DESC) - 时间范围比状态码更容易走索引 range 扫描,尤其当索引是
(status, created_at)时 - CTE 里别写
SELECT *,大字段(如description)会放大width,拖慢整个窗口物化过程
别信 OVER () 就不用排序
即使写 OVER ()(无分区、无 ORDER BY),PostgreSQL 仍可能插入 Sort 节点——这是为保证结果确定性,堆表物理顺序不稳。
- 如果业务能接受稳定但非业务意义的顺序,加
ORDER BY ctid有时能跳过额外排序 - 用
CLUSTER或按created_at建索引让数据物理有序,优化器可能感知到“已排序”而省掉Sort - 混用
ROW_NUMBER() OVER ()和AVG(x) OVER ()会强制排序,其实后者不需要——拆开写或换用聚合子查询更轻量
真正卡住的从来不是语法,是执行路径上那一层层没拦住的数据。查计划、砍输入、建对索引,三步做完,磁盘写入通常就消失了。

















