排序落盘后,团队把 work_mem 从 4MB 全局调到 256MB。单条报表快了,BI 高峰数据库却被 OOM Killer 终止。直接答案是:work_mem 约束的是一次 Sort、Hash 等操作的基础预算,不是一条查询、更不是整个实例的内存上限;多个操作、并行参与者与并发查询会叠加,Hash 还会乘 hash_mem_multiplier。
work_mem是许多 Sort、Hash 等操作各自的基础预算。多个节点、Hash 倍数、并行参与者和并发连接会继续相乘;允许受控落盘通常比全局追求零临时文件更安全。

先用压力模型代替推荐值
text
单个查询的执行内存压力
≈ Σ(同时活跃 Sort 的各自预算)
+ Σ(同时活跃 Hash 的 work_mem × hash_mem_multiplier)
再按实际并行参与者及其执行节点放大
实例峰值
≈ 上述查询内存
× 同时执行的重查询数
+ shared_buffers
+ backend/连接基础内存
+ maintenance/autovacuum/扩展等其他内存
这不是上界公式:节点生命周期未必重叠,executor 还有不完全受 work_mem 约束的结构,实际结构也可能提前 spill,leader 与 worker 分工随计划变化。它的用途是识别风险因子,不能拿来承诺"最多占多少"。它足以否定 max_connections × work_mem 或"每条查询最多 256MB"这两种简单口径。
PostgreSQL 文档明确指出,一个复杂查询可能有多个 Sort/Hash 操作,并发会话可能同时使用各自预算。Hash 操作的上限还会乘 hash_mem_multiplier。
源码证明:Sort 与 Hash 拿的是不同操作预算
PostgreSQL 18.6 中,两条关键源码路径把配置映射成 executor 状态:
src/backend/utils/sort/tuplesort.c的tuplesort_begin_common()为一次排序 创建独立的TupleSort main、TupleSort sortMemoryContext,并把该调用收到的workMem换算后写入allowedMem。排序数据超过预算后,tuplesort 转为临时 runs 和外部归并;这对应 EXPLAIN 的Sort Method、Memory、Disk与 temp blocks。src/backend/executor/nodeHash.c的get_hash_memory_limit()直接计算work_mem × hash_mem_multiplier × 1024。这对应 Hash 节点的Memory Usage、Batches和Disk Usage;增大预算可能减少 batch,但预算是每个相关 Hash 操作的,不是整条计划共享一个固定块。
text
SET LOCAL work_mem
├─ Sort → tuplesort_begin_common(workMem) → allowedMem → 内存排序或临时 runs
└─ Hash → get_hash_memory_limit() → work_mem × hash_mem_multiplier → 内存表或 batches
EXPLAIN (ANALYZE, BUFFERS)
└─ Sort Memory/Disk、Hash Memory/Batches/Disk、temp read/write
SQL 是 PostgreSQL executor 的正式入口,本文要证明的是服务端算子预算与实例内存放大,不存在能比 SQL + EXPLAIN 更直接的 Java 入口,因此不添加与结论无关的 JDBC 包装代码。
一个查询同时触发 HashAggregate 与 Sort
在资源受控的测试库运行,数据规模按机器调整:
sql
DROP TABLE IF EXISTS mem_demo;
CREATE TABLE mem_demo AS
SELECT
g AS id,
md5(g::text) AS k,
g % 100000 AS group_id,
random() AS v
FROM generate_series(1, 3000000) AS g;
ANALYZE mem_demo;
低内存实验使用事务级设置,避免污染连接后续请求:
sql
BEGIN;
SET LOCAL work_mem = '4MB';
SET LOCAL max_parallel_workers_per_gather = 0;
EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
SELECT group_id, count(*) AS cnt, avg(v)
FROM mem_demo
GROUP BY group_id
ORDER BY cnt DESC, group_id;
ROLLBACK;
记录执行计划中的:
- Sort Method、Memory、Disk;
- HashAggregate 的 Batches、Memory Usage、Disk Usage;
- temp read/write;
- 总执行时间与 shared buffers。
再提高当前事务预算:
sql
BEGIN;
SET LOCAL work_mem = '256MB';
SET LOCAL max_parallel_workers_per_gather = 0;
EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
SELECT group_id, count(*) AS cnt, avg(v)
FROM mem_demo
GROUP BY group_id
ORDER BY cnt DESC, group_id;
ROLLBACK;
高预算可能减少 Hash batches 和 Sort 落盘,单查询更快;但它只证明这条查询在单并发、禁用并行时受益,不能支持全局修改。数据是否已缓存、实际 group 数和 planner 选择都会影响结果。
同一条计划为什么能消费多份预算
一个执行计划可能同时或交叠存在:
- 多个 Sort,例如窗口函数、Merge Join 与最终 ORDER BY;
- Hash Join 的 build hash table;
- HashAggregate;
- Memoize、Materialize、去重等其他状态;
- 子查询与 CTE 内各自的操作;
- 并行 worker 中重复执行的节点。
work_mem 不是预先为每个连接一次性保留的固定块;这解释了空闲连接不等于都占满 work_mem。但高并发计划同时达到预算时,总量会急剧上升。
Hash 为什么还要乘一次
sql
SHOW work_mem;
SHOW hash_mem_multiplier;
Hash-based 操作可使用 work_mem × hash_mem_multiplier。如果 work_mem = 256MB 且 multiplier 为 2,一个 Hash 节点的基准上限思路已接近 512MB;同一查询再有第二个 Hash 与 Sort,就不是"256MB 查询"。
增大 multiplier 能减少 batch,也会放大峰值。它适合在确定 Hash spill 是主要瓶颈且总量可控时局部验证,不应与 work_mem 同时盲目上调。
并行查询如何继续放大
sql
EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
SELECT group_id, count(*)
FROM mem_demo
GROUP BY group_id;
查看 Workers Planned 与 Workers Launched。并行查询使用的资源可能远多于非并行计划,因为每个 worker 是独立进程,某些操作在多个参与者中各自分配内存;leader 也可能参与执行。
Workers Planned = 4 不保证实际启动 4 个,实际资源竞争会影响 launched 数。容量压测要以 Workers Launched、真实并发和进程 RSS 为证据,不能只把配置上限代入公式后声称精确。
官方文档给出的容量含义更直接:并行查询中的 work_mem 等资源限制通常逐 worker 生效,4 个 worker 加 leader 的资源消耗可能接近非并行查询的 5 倍,但并非每个节点都会在每个参与者中同时达到预算。并行维护命令的 maintenance_work_mem 又是整条命令共享,不能把查询规则机械套到 CREATE INDEX 或 VACUUM。
把单查询收益升级为并发证据
单会话 EXPLAIN 只能证明一次执行。要评估全局修改,需在有明确内存限制的隔离实例中增加并发,先从较小预算开始。示例 work_mem-case.sql:
sql
BEGIN;
SET LOCAL work_mem = '32MB';
SET LOCAL max_parallel_workers_per_gather = 0;
SELECT count(*)
FROM (
SELECT group_id, count(*) AS cnt, avg(v)
FROM mem_demo
GROUP BY group_id
ORDER BY cnt DESC, group_id
) AS report_result;
ROLLBACK;
bash
pgbench -n -c 4 -j 4 -T 60 -f work_mem-case.sql target_database
每一级并发同时采集数据库/容器 RSS、swap/OOM、临时文件增量、吞吐和 p95/p99;若内存逼近隔离环境上限或开始 swap,立即停止,不继续把 -c 放大。该命令仅用于有资源上限的隔离压测,禁止直接指向生产库。
为什么"临时文件为零"不是优化目标
sql
SELECT
datname,
temp_files,
pg_size_pretty(temp_bytes) AS temp_bytes
FROM pg_stat_database
ORDER BY temp_bytes DESC;
该视图是数据库级累计统计,不能直接归因到某条 SQL,且统计可能有刷新延迟或重置。可结合 log_temp_files、pg_stat_statements 和 EXPLAIN 定位具体工作负载。
临时文件是受控退化路径。一个每天运行一次的报表 spill 10GB,可能比 100 个并发报表各自多拿数百 MB 更安全。真正目标是业务吞吐、尾延迟、内存水位与临时盘容量共同满足,而不是把 temp_bytes 清零。
还要区分"慢但可完成"和"内存把实例杀掉":前者影响单任务,后者会让所有连接中断并触发恢复。
用 MemoryContext 证据补足进程 RSS
操作系统 RSS 能证明进程当前占用,但不能告诉你是哪一个 PostgreSQL 执行上下文在增长。PostgreSQL 18 可请求目标 backend 把 MemoryContext 树写入服务端日志:
sql
SELECT pg_log_backend_memory_contexts(<target_pid>);
<target_pid> 应来自已经确认的测试会话或问题查询 PID,不能照抄占位符。该函数会向目标 backend 发信号,默认需要较高权限;输出进入服务端日志而不是 SQL 结果,因此还要控制日志访问和敏感信息范围。
建议在隔离压测中按时间线采样:
text
T0:查询启动前记录实例/容器内存与 PID
T1:Hash/Sort 进入高水位时请求 MemoryContext 日志
T2:查询结束后再次记录 RSS、temp I/O 与上下文释放
MemoryContext 日志可以定位 executor、tuple sort、hash 等上下文的分配方向,但它是一个时点快照;不能直接相加为整条查询历史峰值,也不能替代操作系统和容器总量监控。
先修计划,再给内存
Hash/Sort 过大有时是基数估算错误造成的。推荐顺序:
- 找第一处 estimated/actual rows 分叉,修统计、过滤和 Join 顺序问题。
- 减少无用返回列和中间行,尽早过滤。
- 验证索引是否能提供过滤或排序,避免本不必要的全量 Sort。
- 对确定受益的角色、事务或作业局部提高
work_mem。 - 同时限制 BI 并发与并行度,再做混合负载压测。
若第一处 estimated/actual rows 已明显分叉,先按第 9 篇:SQL 和索引没变,计划为什么突然慢一百倍修正统计与计划;给错误计划更多内存,只会让错误路径跑得更昂贵。
局部示例:
sql
BEGIN;
SET LOCAL work_mem = '64MB';
SET LOCAL hash_mem_multiplier = 1.5;
-- 已验证且受并发控制的报表查询
COMMIT;
若经连接池执行,必须保证事务边界可靠;使用 session 级 SET 后忘记 RESET,可能把高预算泄漏给后续用户。
不能漏算的其他内存池
实例容量还包括:
shared_buffers;- 每个 backend 的基础状态、缓存和查询上下文;
maintenance_work_mem对 CREATE INDEX、VACUUM 等维护操作;- autovacuum worker 的维护内存边界;
- WAL、锁表、连接认证、扩展与语言运行时;
- 操作系统页缓存及其他进程。
因此"物理内存减 shared_buffers,剩余全分给 work_mem"没有为峰值抖动、内核和维护任务留下安全余量。容器环境还要以 cgroup limit 而非宿主机总内存为边界。
生产灰度与停止条件
变更前记录:查询 p50/p95/p99、规划和执行时间、temp bytes、进程/容器 RSS、swap、OOM 事件、并发数、workers、业务吞吐。
灰度单位优先是单个报表角色或作业队列,而非全实例。先限制并发为 1---2,逐级增加;每级覆盖完整报表周期。验收要求报表 SLA 改善,同时数据库内存高水位、OLTP 延迟和临时盘都在预算内。
出现以下任一信号立即停止扩大:
- 可用内存逼近预设安全线或开始持续 swap;
- postmaster/backend 被 OOM killer 处理;
- OLTP p99 或错误率超过阈值;
- 临时盘仍增长而内存也显著升高;
- 并发稍增就出现非线性抖动。
恢复原 work_mem 可以限制后续分配,但不能撤销当前已运行查询占用,也不能消除 OOM 后恢复影响。必要时先停止新报表进入,再等待或按业务可重试性取消明确的重查询。
证据边界
| 证据 | 能证明 | 不能证明 |
|---|---|---|
| 高 work_mem 单查询更快 | 该查询减少 spill 后受益 | 全局高并发安全 |
| temp bytes 很高 | 数据库产生大量临时 I/O | 唯一 SQL 和根因 |
| Sort Memory 为 200MB | 该 Sort 节点使用量 | 整条查询总内存 |
| workers 为 4 | 计划/执行使用并行参与者 | 每个 worker 都吃满同样内存 |
| OOM 后调回配置 | 后续预算恢复 | 当前查询和实例影响已回滚 |
面试表达主线
work_mem 是 Sort/Hash 等操作级预算,一条查询可有多个操作,Hash 再乘 hash_mem_multiplier,并行 worker 与并发连接继续放大。调优要看 EXPLAIN 的 Memory、Disk、Batches 与 temp I/O,优先局部设置和并发治理;可控 spill 通常比全局大内存安全。
实验清理
sql
DROP TABLE IF EXISTS mem_demo;
官方资料
- PostgreSQL 18:Resource Consumption
- PostgreSQL 18:Using EXPLAIN
- PostgreSQL 18:How Parallel Query Works
- PostgreSQL 18:Runtime Statistics
- PostgreSQL 18:Server Signaling and Memory Context Logging
- PostgreSQL 18.6 源码:tuplesort.c / tuplesort_begin_common()
- PostgreSQL 18.6 源码:nodeHash.c / get_hash_memory_limit()
- PostgreSQL 18.6 源码标签 REL_18_6