你以为把 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 并不夸张;这不是 PostgreSQL “不遵守 work_mem”。
而是你把 work_mem 当成了硬上限,而它从来都不是。
work_mem 真正控制的,是“是否开始落盘”;它控制不了“整条执行链最终占多少内存”官方表述其实已经给出答案:work_mem 是“在写临时磁盘文件之前”的内存预算。
注意这句话的关键词:
before writing to temporary disk files
也就是说,它更像是一个节点级的内存-磁盘切换阈值。 它能影响 sort / hash 是更愿意在内存里做,还是更早 spill 到磁盘。 但它并不能替代下面这些问题:
所以, 把 work_mem 调大,常常不是解决问题,而是在隐藏问题。
在小并发、强控制、偏分析型 workload 里,调大 work_mem 当然可能有收益;PostgreSQL 官方也明确说了,在 hash 经常 spill,而提高 work_mem 又带来内存压力时,可以考虑用更高的 hash_mem_multiplier,甚至在某些环境中提升到 2.0–8.0 以上。
但这里有一个不可跳过的前提:
前提一:并发是可控的。 前提二:查询形态是可控的。 前提三:执行计划足够稳定。
如果这三个前提不成立,调大 work_mem 往往不是优化,而是把单点慢 SQL 升级成整库级故障。
这才是 DBA 和开发最应该正视的现实。
原文里的事故,本质上不是“参数设错了”,而是查询写法和执行生命周期设计出了问题。复盘中提到:一个 SELECT 调用了 plpgsql 函数,函数内部还做了 COPY 相关操作,再把结果拿去 JOIN。语法上合法,但从执行器角度看,这可能把原本应该切分、释放、落盘的东西,拼成了一个超长生命周期的大操作。
这背后有两个基本规律:
如果统计信息失真,Planner 低估行数,就更容易选出本该 spill 却没及时 spill、或者本不该 hash 却选了 hash 的计划。PostgreSQL 官方也明确建议,复杂查询可能同时进行多个 sort/hash;而是否会落盘、落到什么时候,本来就强依赖计划和执行路径。
很多开发喜欢把逻辑包进函数、子查询、CTE、视图,然后再层层 JOIN,觉得“代码优雅了”。 但数据库不是应用层对象系统。 数据库优化器看的是数据流、算子、生命周期、代价模型,不是“抽象是否漂亮”。
对 PostgreSQL 来说, 好维护的代码结构,不一定等于好执行的 SQL 结构。
这是很多团队直到出事故都没真正接受的事实。
在这类问题上,很多人本能会问:
“那有没有一个参数,限制单连接最多吃多少内存?”
很遗憾,没有。 至少没有一个像很多人想象中的、简单直接、可靠的“per-backend hard cap”配置。原文对此说得很直白: 你不能对 PostgreSQL 每个 backend 设一个硬性内存封顶。
你能做的,是一整套“风险治理”组合拳,而不是迷信一个参数:
这才是工程化思路。
不是“把 work_mem 调小一点,求神保佑”。
PostgreSQL 已经给了你非常好用的工具:pg_log_backend_memory_contexts(pid)。
官方文档说明,它会把指定 backend 的 memory contexts 以 LOG 级别打进日志;而且要注意,这个函数可能带来明显开销,因为它会生成大量日志。
这类工具为什么重要?
因为过去很多团队排查内存问题,方法都非常原始:
pg_stat_activity但真正的关键问题是:
内存到底堆在哪个上下文里? 是谁分配的? 为什么没释放?
没有 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 往往是合理的:
如果这些前提成立,调大 work_mem 是优化。
如果这些前提不成立,调大 work_mem 是赌博。
一字之差,结果天差地别。
原文里最值得反复咀嚼的一句判断是:
不是硬件不够大,而是查询写法错了。 哪怕你有 2TB RAM,照样可能被一条“看起来没什么”的 SQL 拖垮。
所以真正成熟的 PostgreSQL 团队,不会把 work_mem 当成万能旋钮。
他们会把它看成:
说白了:
work_mem不是性能开关。 它是资源杠杆。 杠杆用对了,省力;用错了,砸库。
你们线上出过和 work_mem、临时文件、Hash Join、并行查询有关的事故吗?
你更认同“参数先行”,还是“SQL 与统计信息优先治理”?
你怎么看,评论区聊聊。