乐于分享
好东西不私藏

work_mem:AI 帮你算,你别乱猜

work_mem:AI 帮你算,你别乱猜

导读

这是《AI 助力 PostgreSQL 优化 20 讲》第 6 讲,内存与 I/O 调优小系列(06-08 讲)的第一篇。 上一讲讲完索引设计,这一讲进内存调优。work_mem 是最容易被误解的 PostgreSQL 参数之一——设大了 OOM,设小了查询慢,两头都是坑。本讲解释它是什么、陷阱在哪、以及如何用 AI 辅助算出合理值。适合有 PostgreSQL 使用经验的研发和 DBA。

开篇:work_mem=2MB,怎么还是 OOM 了

生产事故记录(来源:mydbanotebook.org):

某集群把 work_mem 设成了 2MB。不是 512MB,是 2MB。然后在高峰期触发了 OOM killer,内存消耗峰值逼近 2TB

复盘结果:一条 SELECT 语句调用了 PL/pgSQL 函数,函数里做了 COPY,再 JOIN 另一张大表。执行器内存快照显示:

ExecutorState 上下文:235MB(40 个内存块)

HashTableContext:340MB(47 个内存块)

单个 backend 总消耗:557MB

work_mem=2MB,单个 backend 却用了 557MB——参数根本没拦住。原因下面解释。

work_mem 是什么:逐字读懂它

先看官方定义里最关键的一句话:

“a complex query might perform several sort and hash operations at the same time, with each operation generally being allowed to use as much memory as this value specifies”

拆开看:

per-operation:每个 Sort / Hash 操作各自独立分配,互不干涉

at the same time:同一查询的多个操作可以同时占用内存

this value:每个操作的上限是 work_mem,不是所有操作共享一个池

所以 work_mem 不是”数据库的内存配额”,是”每个操作节点的最高许可证”。

理解这个之后,乘法炸弹就很清晰了:

200 个并发连接,每个查询有 5 个 Sort/Hash 节点,work_mem=512MB,理论峰值 = 500GB。没有服务器能顶住这个。

陷阱:内存什么时候释放

回到开头那个案例。work_mem=2MB,为什么还是用了 557MB?

原因在 PostgreSQL 的内存管理机制:所有 Sort/Hash 操作分配的内存,都挂在同一个 ExecutorState 内存上下文下。这个上下文在整个查询结束前不释放。

PostgreSQL 采用”上下文整体释放”的设计——释放速度快,但代价是粒度粗。查询一直跑,内存一直占。如果查询本身出了问题(死循环、超大结果集、函数嵌套),内存会一直累积到 OOM。

这就是为什么 statement_timeout 是 work_mem 的配套安全阀。没有超时保护,一条失控的查询可以把服务器内存耗尽,跟 work_mem 设多少无关。

两种 Spill:Sort 和 Hash Join

数据超出 work_mem 限制时,PostgreSQL 不会报错,而是把数据写到临时文件继续处理——这就是 spill to disk。

Sort 节点的 spill:排序数据超出 work_mem 时,进入 external merge sort。EXPLAIN ANALYZE 里看到 Sort Method: external merge Disk: 142MB 就是在 spill。

Hash Join 的 spill:触发阈值比 Sort 更早,内表超过一定比例后就开始分批。每批处理一部分数据,剩余写临时文件。Batches: 8 代表分了 8 批,说明在磁盘上走了 7 次 I/O。高频 join key 会优先留在内存(skew 优化),能减少部分磁盘读写,但代替不了足够的 work_mem。

Spill 的代价是磁盘 I/O,不是查询失败,所以很容易被忽视。等到业务反馈”查询变慢了”,临时文件可能已经堆了几十 GB。

发现问题:三个工具

工具一:EXPLAIN (ANALYZE, BUFFERS)

最直接。看两个关键字段:

EXPLAIN (ANALYZE, BUFFERS)SELECT ...;

Sort 节点:找 Sort Methodexternal merge Disk: XXX = 在 spill

Hash 节点:找 Batches,大于 1 = 在 spill

工具二:log_temp_files

-- postgresql.conflog_temp_files = 0   -- 记录所有临时文件(0 = 全部记录,单位 kB)

开启后,每次有临时文件产生,日志里会记录大小。统计高峰期每小时产生多少临时文件、最大的有多大——这是衡量当前 work_mem 够不够的最直接数据。

工具三:pg_log_backend_memory_contexts()(PG14+)

-- 找到目标进程的 PIDSELECT pid, query FROM pg_stat_activity WHERE state = 'active';-- 把该 backend 的内存上下文树写到日志SELECT pg_log_backend_memory_contexts(pid);

日志里会出现类似这样的信息:

ExecutorState: 235 MB in 40 blocksHashTableContext: 340 MB in 47 blocks

这是最细粒度的内存分析工具。遇到内存异常,先用这个定位是哪个操作在吃内存,再决定怎么调。

给 AI 算合理值

知道了乘法炸弹的结构,计算合理值的逻辑就清楚了:

全局保守值 = 安全内存上限 ÷ (峰值连接数 × 单查询最大操作数)

这个计算 AI 能做,但你必须给够上下文。

Prompt 模板:

我的 PostgreSQL 集群配置如下:- 服务器内存:128GB,PG 可用内存约 80GB- 峰值并发连接:300(其中约 20% 是复杂查询)- 分析过几条典型慢查询,最多的有 8 个 Sort/Hash 节点- log_temp_files 显示高峰每小时约 200 个临时文件,最大单个 512MB- 当前 work_mem=4MB请帮我:1. 计算合理的全局 work_mem 值2. 估算调整后的最坏情况内存占用3. 建议哪类查询需要单独 SET work_mem 提高,以及建议值

AI 会给你:全局推荐值(通常 8-32MB 区间)、最坏情况内存占用验算、需要单独处理的重查询类型。

AI 判断不了的:实际并发峰值分布。300 个连接里同时跑复杂查询的实际比例,只有你的监控数据说了算。上下文里给的数字越准,AI 的推荐越有参考价值。

两种用法的组合

全局:设保守值

-- postgresql.confwork_mem = 16MB   -- 全局保守,防乘法炸弹statement_timeout = '30s'  -- 配套安全阀,防失控查询log_temp_files = 0  -- 持续监控 spill

会话级:对重查询放大

对于已知的报表查询、数据导出任务,在会话内临时提高:

SET work_mem = '256MB';-- 执行重查询RESET work_mem;

这种用法适合能控制会话生命周期的场景(后台任务、脚本、专用连接)。连接池复用的短连接不适合这种方式,容易出现设置泄漏。

验证:改完怎么确认有效

改了 work_mem 之后,三个指标确认效果:

指标
改前
改后(期望)
EXPLAIN Sort Method
external merge Disk quicksort Memory
EXPLAIN Hash Batches
Batches: 8 Batches: 1
log_temp_files 数量
200 个/小时
明显下降

如果 Sort Method 还是 Disk,说明数据量超出了新的 work_mem 上限,要么继续调大(先评估乘法),要么从查询层面优化(加索引、减少排序字段)。

一个结论收尾

内存调优最常见的错误是把它当成参数问题。mydbanotebook.org 的作者说得对:没有任何硬件能抗住一条失控的查询。 work_mem 控制的是正常查询的内存使用,控制不了失控的查询。真正的安全线是 statement_timeout + 监控 + 定期 EXPLAIN 分析,这三件事做到位,参数才有意义。

下一讲:shared_buffers 与缓存命中——另一个经常被设错的内存参数,以及用 pg_buffercache 看清楚内存里到底缓存了什么。

本文由人类 DBA + AI 协作完成。观点和案例是人的,查文档和码字是 AI 的。所有踩过的坑——那是人的专属贡献,AI 没有生产权限。