work_mem 到底什么时候释放? 很多人理解错了很多人对 PostgreSQL 的 work_mem,其实一直有个误解。
最典型的问题就是这几个:
work_mem 是不是一申请就占满?如果你也有过这些疑问,那么这篇文章想讲清楚一件事:
PostgreSQL 里和 work_mem 相关的内存,绝大多数情况下并不会等到事务结束才释放。很多内存其实会在 SQL 执行过程中就被提前回收。
而且,排序、Hash Join、Hash Aggregate 这三类算子的释放方式还不完全一样。
这也是为什么很多人明明配了很大的 work_mem,却仍然搞不清楚 PostgreSQL 的真实内存峰值到底从哪来。
work_mem 不是“申请了就一直占着不放”一句话先说透:
work_mem 不是预分配内存,而是排序、哈希等执行操作的一个软预算。
也就是说:
更准确的规律其实是:
如果你以前理解成“work_mem 申请后会一直保留到事务提交”,那这个认识要更新了。
work_mem因为从参数名字看,work_mem 很容易让人误以为:
“这是 PostgreSQL 给一个 SQL 分配的一块工作内存。”
但实际上,它更接近于:
SQL 执行过程中,某些需要排序或哈希工作区的操作可使用的内存预算上限。
注意这里有两个关键词:
这意味着什么?
意味着一个 SQL 里如果有多个 Sort、多个 Hash Join、多个 Hash Agg,它们可能分别使用自己的预算。 如果再叠加并行执行,实际内存消耗还会进一步放大。
所以你看到 work_mem = 64MB,并不等于“这个 SQL 最多只用 64MB”。
这也是 PostgreSQL 内存调优里最容易踩坑的地方之一。
另外,Hash 类操作通常还会乘上 hash_mem_multiplier,所以它们的可用预算往往比 Sort 更大。
先看排序。
排序主要由 tuplesort.c 管理。它会创建几层 memory context,用来保存排序过程中的元数据、tuple 数据、merge 状态等。
但真正重要的不是“创建了几个 context”,而是:
排序过程中确实存在不少提前释放动作,但不是所有排序相关内存都会立刻释放。
比如:
也就是说,排序并不是“先攒一堆,最后统一释放”。
它更像是:
一边处理,一边把已经没用的那部分工作内存尽快回收。
但这里有个容易忽略的细节:
Sort 的释放并不总是“立刻彻底释放”。
tuplesort 内部有 maincontext 和 sortcontext 之分。
执行过程中经常被 reset 的,主要是 sortcontext 以及它下面的 tuple 工作内存;
而 maincontext 里保存的 Tuplesortstate 本体、排序键元数据等,可以跨多个 batch 复用。
这在 Incremental Sort 里尤其明显。增量排序会反复调用 tuplesort_reset() 来清理当前 batch 的排序工作区,但 maincontext 仍然保留,以便下一批继续复用。真正彻底释放,要等到节点结束时调用 tuplesort_end()。
所以对于 Sort 来说,更准确的说法应该是:
大量排序工作内存会在执行过程中提前回收,但部分排序元数据和状态可能会保留到节点结束。
再看 Hash Join。
Hash Join 的思路和 Sort 不太一样,它不是那种“经常细粒度释放”的风格,而更偏向于:
按 batch 批量处理,按 batch 批量释放。
它内部大致会有几类内存:
Hash Join 最重要的释放动作是:
每处理完一个 batch,就 reset 这个 batch 的内存 context。
这意味着:
pfree如果内存继续上涨怎么办?
PostgreSQL 的策略不是“硬扛”,而是:
这就是为什么 Hash Join 在内存不够时,往往表现为“分批写盘”,而不是简单失控。
另外,Hash Join 在某些路径下也会做更早的回收,比如:
batchCxt换句话说,Hash Join 的内存管理核心不是“精细到每条 tuple 都单独释放”,而是:
用 batch、spill 和批量 reset,把整体内存控制在一个可收敛的范围内。
如果说 Sort 是“边做边释放”,Hash Join 是“按 batch 释放”,那么 Hash Agg 可以说是:
释放粒度很多、回收动作很积极的一类。
但这里有一个很重要的纠偏点:
不能简单说成 Hash Aggregate 每完成一个 group,就会把这个 group 的聚合状态立刻释放。
更准确地说,聚合相关清理有几种不同粒度:
tmpcontext,释放这条 tuple 的临时表达式计算内存ReScanExprContext(),释放已完成 group 的状态ExecEndAgg() 才会做最终清理如果内存超限,Hash Agg 会进入 spill 模式:
在处理每个 spilled batch 前,还会重置相关 hash 工作内存,让下一批重新获得预算空间。
所以更准确的说法应该是:
Hash Agg 的内存回收非常积极,但它的主要释放粒度是 per-tuple、spill batch 切换和节点结束,而不是简单概括成“每个 group 完成就立刻释放”。
work_mem看到这里,其实就能把 work_mem 的概念纠正过来了。
work_mem 不是:
而更应该理解为:
执行器里某些排序/哈希类操作可用的软预算,实际内存会随着执行过程动态分配、动态回收,超限后再通过 spill 转移到磁盘。
所以,调优 work_mem 这件事,重点从来都不只是“值设多少”。
更重要的是理解:
work_mem 的排序或哈希操作因为现实里很多 PostgreSQL 的内存问题,并不是“不会释放”,而是“你低估了会同时申请预算的节点数量”。
举个非常常见的误区:
有人看到磁盘 spill,第一反应就是:
“work_mem 太小了,调大。”
但问题是,如果一条复杂 SQL 本来就有很多排序和哈希操作,你把 work_mem 从 64MB 调到 256MB,单个操作确实更不容易 spill 了,但整体内存峰值可能反而暴涨。
结果就是:
所以 spill 不是绝对的坏事。 很多时候,适度 spill 其实是 PostgreSQL 在“内存安全”和“执行效率”之间做的合理平衡。
最后把这件事收成一句话:
PostgreSQL 中 work_mem 相关内存,并不是等到事务结束才释放,而是大量内存在执行过程中就会按不同粒度被提前回收。
但也不要把这件事理解得过于绝对:
所以,理解 PostgreSQL work_mem 的关键,不是只记住参数值,而是理解它背后的执行器行为。
只有这样,你才能真正看懂:
work_mem 不能简单粗暴地无限调大如果你之前也一直把 work_mem 理解成“事务级保留内存”,那现在可以把这个旧印象彻底改掉了。
本文基于 PostgreSQL 源码阅读与 DeepWiki 辅助分析整理。
参考链接: https://deepwiki.com/search/postgresql-workmem-sql_281d8b2c-c9af-4594-b743-6ff484c6bbab