你信吗? 2TB内存数据库发生OOM?
你以为把 work_mem 从 4MB 调到 64MB,是在“优化排序和哈希”?
很多时候,你不是在提速。
你是在把一颗延时爆炸的内存雷埋进生产库。更扎心的是:
就算你把 work_mem 设成 2MB,也不代表 PostgreSQL 一条查询最多只会吃 2MB。
一、先说结论:work_mem 最大的坑,不是你不会调,而是你以为自己懂它
PostgreSQL官方文档写得很清楚:work_mem 规定的是单个查询操作在写临时文件前可使用的基础内存上限,典型对象包括 sort 和 hash table;复杂查询里可能同时存在多个 sort/hash,多个会话还能并发执行, 总内存消耗可能是 work_mem 的很多倍。更要命的是,哈希类操作的上限还会再乘以 hash_mem_multiplier,默认就是 2.0。并行查询时,每个 worker 还会各自独立吃这份资源。官方甚至直接提醒:一个 4-worker 的并行查询,资源占用可能接近无并行时的 5 倍。
翻译成人话就是:
work_mem 从来不是“每条 SQL 的内存上限”,更不是“这台机器的安全阈值”。
它只是执行计划里某个内存操作节点的预算起点。
所以,很多团队一上来就问:“我们 256GB 内存,work_mem 设 64MB 保守吗?”
这问题从第一性原理上就问错了。
你不先问并发数、执行计划里有多少内存节点、是否启用并行、哈希是否放大、是否有错误估算,单独讨论 work_mem 数字,基本等于闭眼开车。
二、为什么 work_mem 明明不大,库还是会被打爆?
这是最容易被误解的地方。
很多人知道一个常识:work_mem 不是“每查询”,而是“每操作”。这没错。
但只知道这一层,还不够。
最近一篇 PostgreSQL 实战文章里记录了一个非常典型的事故:生产集群曾被 OOM killer 干掉,机器吃掉了 2TB RAM;而 work_mem 当时只有 2MB。作者复盘时,在复现环境里通过 pg_log_backend_memory_contexts 抓到了后端内存上下文:单个 backend 的日志里,ExecutorState 约 235MB,HashTableContext 约 340MB,总计约 557MB,其中 ExecutorState 还出现了 524,059 chunks。
这件事说明什么?
说明真正的危险,不只是“有很多 sort/hash 节点”,而是:这些内存未必会在过程中及时归还。
PostgreSQL 的内存管理核心机制是 memory context。它设计的优势就是:不按对象逐个 free,而是按上下文整块释放。
官方/社区对这一机制的解释非常一致:memory context 的主要优势,是能“一次性释放整个上下文里的内容”,这样更快,也更可靠。
问题也正出在这里。
如果某些分配挂在一个生命周期很长的上下文上,比如 ExecutorState,那它们就可能一路攒着,直到整个执行结束才释放。如果查询本身很大、执行很久、又因为函数封装或糟糕的 SQL 结构把本来可以切开的步骤揉成了“一锅大杂烩”,内存就会像滚雪球一样涨。
所以你看到的现象是:
配置上 work_mem 并不夸张
单看某个节点预算也不离谱
但执行期间,多个节点、多个 worker、多个分配上下文叠加
再加上长生命周期上下文延迟释放
最后,操作系统出来“物理教你做人”
这不是 PostgreSQL “不遵守 work_mem”。而是你把 work_mem 当成了硬上限,而它从来都不是。
三、work_mem 真正控制的,是“是否开始落盘”;它控制不了“整条执行链最终占多少内存”
官方表述其实已经给出答案:work_mem 是“在写临时磁盘文件之前”的内存预算。
注意这句话的关键词:
before writing to temporary disk files
也就是说,它更像是一个节点级的内存-磁盘切换阈值。
它能影响 sort / hash 是更愿意在内存里做,还是更早 spill 到磁盘。
但它并不能替代下面这些问题:
执行计划是不是错了?
行数估算是不是低得离谱?
一个查询里是不是同时堆了太多 hash/sort/memoize?
并行是不是把放大器打开了?
SQL 是否把多个阶段硬捏成了一个长生命周期执行体?
函数、CTE、子查询、JOIN 方式是否让释放时机变晚?
所以, 把 work_mem 调大,常常不是解决问题,而是在隐藏问题。
在小并发、强控制、偏分析型 workload 里,调大 work_mem 当然可能有收益;PostgreSQL 官方也明确说了,在 hash 经常 spill,而提高 work_mem 又带来内存压力时,可以考虑用更高的 hash_mem_multiplier,甚至在某些环境中提升到 2.0–8.0 以上。
但这里有一个不可跳过的前提:
前提一:并发是可控的
前提二:查询形态是可控的
前提三:执行计划足够稳定
如果这三个前提不成立,调大 work_mem 往往不是优化,而是把单点慢 SQL 升级成整库级故障。
四、真正该背锅的,常常不是参数,而是 SQL 结构和统计信息
这才是 DBA 和开发最应该正视的现实。
原文里的事故,本质上不是“参数设错了”,而是查询写法和执行生命周期设计出了问题。复盘中提到:一个 SELECT 调用了 plpgsql 函数,函数内部还做了 COPY 相关操作,再把结果拿去 JOIN。语法上合法,但从执行器角度看,这可能把原本应该切分、释放、落盘的东西,拼成了一个超长生命周期的大操作。
这背后有两个基本规律:
如果统计信息失真,Planner 低估行数,就更容易选出本该 spill 却没及时 spill、或者本不该 hash 却选了 hash 的计划。PostgreSQL 官方也明确建议,复杂查询可能同时进行多个 sort/hash;而是否会落盘、落到什么时候,本来就强依赖计划和执行路径。
很多开发喜欢把逻辑包进函数、子查询、CTE、视图,然后再层层 JOIN,觉得“代码优雅了”。
但数据库不是应用层对象系统。
数据库优化器看的是数据流、算子、生命周期、代价模型,不是“抽象是否漂亮”。
对 PostgreSQL 来说, 好维护的代码结构,不一定等于好执行的 SQL 结构。
五、DBA 最该告诉业务方的一句话:你不能给 PostgreSQL 设“每个 backend 的硬内存上限”
这是很多团队直到出事故都没真正接受的事实。
在这类问题上,很多人本能会问:“那有没有一个参数,限制单连接最多吃多少内存?”
很遗憾,没有。
至少没有一个像很多人想象中的、简单直接、可靠的“per-backend hard cap”配置。原文对此说得很直白: 你不能对 PostgreSQL 每个 backend 设一个硬性内存封顶。
你能做的,是一整套“风险治理”组合拳,而不是迷信一个参数:
控制并发
修正统计信息
改写 SQL
限制并行
设置超时
观察实际内存上下文
把大查询和高峰流量隔离
这才是工程化思路。不是“把 work_mem 调小一点,求神保佑”。
六、遇到可疑内存暴涨,别猜,直接看证据
PostgreSQL 已经给了你非常好用的工具:pg_log_backend_memory_contexts(pid)。
官方文档说明,它会把指定 backend 的 memory contexts 以 LOG 级别打进日志;而且要注意,这个函数可能带来明显开销,因为它会生成大量日志。
这类工具为什么重要?
因为过去很多团队排查内存问题,方法都非常原始:
看操作系统 RSS
看 pg_stat_activity
看慢 SQL
猜是 sort 爆了还是 hash 爆了
但真正的关键问题是:
内存到底堆在哪个上下文里?
是谁分配的?
为什么没释放?
没有 memory context 视角,你看到的只是“高血压”;有了它,你才能找到“是哪根血管堵了”。
另外,如果你想看当前会话自己的内存上下文,PostgreSQL 还提供了 pg_backend_memory_contexts 视图;默认只有 superuser 或具备 pg_read_all_stats 权限的角色可读。
七、那到底该怎么管 work_mem?给 DBA 和开发两套答案
1)不要全局盲调大 work_mem
全局大 work_mem,本质是在给所有会话、所有复杂查询、所有并行 worker批量发放内存信用卡。官方文档已经明确说明,总体消耗可能是很多倍。
2)优先做分层配置,而不是“一刀切”
把高并发 OLTP、低并发分析、批处理、报表查询分开。能按 role、database、session、事务级设置的,就不要全局放开。
3)先压风险,再谈提速
高峰业务链路上,先配 statement_timeout,因为它能在语句执行时间过长时直接中止;官方还明确提示,不建议在 postgresql.conf 里对所有 session 粗暴统一设置。
4)把统计信息治理当成容量治理的一部分
不是只盯 CPU、IO、shared buffers。行数估算偏差,本质上就是容量预算偏差。
5)对并行保持敬畏
并行会加速,也会放大。官方直接说了:资源限制是按 worker 单独生效的,4 worker 的并行查询可能吃出接近 5 倍资源。
1)别把数据库函数、CTE、子查询当成“代码封装美学”工具乱嵌套
你看到的是“复用”;优化器看到的可能是“更长的执行生命周期”和“更差的释放时机”。
2)能分阶段物化的,就别硬拼成一个超级 SQL
有些中间结果,就该落临时表、物化结果集或拆分步骤。数据库最怕的是“逻辑优雅、执行灾难”。
3)你以为的“小查询”,在执行器眼里可能是一条大工程流水线
一个 SELECT 里套函数、函数里再跑复杂逻辑,不代表数据库会把它当“小事”。
4)SQL 性能问题,最终都不是字符串问题,而是资源调度问题
内存、磁盘、CPU、锁、并行度、统计信息、数据分布,缺一不可。
八、什么时候“调大 work_mem”是对的?
也不是说 work_mem 永远不能调大。这篇文章最容易被误读成“work_mem 一调就死”,这也不对。
从第一性原理出发,下面这些前提成立时,提升 work_mem 往往是合理的:
并发数低且稳定
查询主要是分析型,排序/聚合/哈希代价高
有足够内存余量
并行度可控
执行计划稳定
已确认瓶颈真的是 spill 到临时文件,而不是估算失真或 SQL 结构错误
如果这些前提成立,调大 work_mem 是优化。
如果这些前提不成立,调大 work_mem 是赌博。
一字之差,结果天差地别。
九、最后一句狠话:没有任何硬件,能拯救一条设计错误的查询
原文里最值得反复咀嚼的一句判断是:
不是硬件不够大,而是查询写法错了。
哪怕你有 2TB RAM,照样可能被一条“看起来没什么”的 SQL 拖垮。
所以真正成熟的 PostgreSQL 团队,不会把 work_mem 当成万能旋钮。
他们会把它看成:
一个执行器节点预算参数
一个磁盘/内存切换阈值
一个会被并发、并行、哈希放大、生命周期拖长的风险源
说白了:
work_mem 不是性能开关。
它是资源杠杆。
杠杆用对了,省力;用错了,砸库。
你们线上出过和 work_mem、临时文件、Hash Join、并行查询有关的事故吗?
你更认同“参数先行”,还是“SQL 与统计信息优先治理”?
活动推荐
5月22日,2026 XCOPS 智能运维管理人年会「广州站」重磅来袭!聚焦大模型迭代、AI Agent 深度应用等技术热点,邀请一众行业领军人物、技术大咖,从技术架构、实战案例到科研成果,与大家一起探索AI应用于智能运维与数据库的最佳方式,共同破解垂类智能体落地、多Agent协同、数据库自治技术工程化、核心系统信创与智能化平衡等现实难题。扫描下方二维码可了解大会详情及报名↓
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721