首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >PG 的 work_mem 到底什么时候释放? 很多人理解错了

PG 的 work_mem 到底什么时候释放? 很多人理解错了

作者头像
用户4035096
发布2026-07-10 09:08:39
发布2026-07-10 09:08:39
10
举报

PG 的 work_mem 到底什么时候释放? 很多人理解错了

很多人对 PostgreSQL 的 work_mem,其实一直有个误解。

最典型的问题就是这几个:

  • work_mem 是不是一申请就占满?
  • 排序、Hash Join、Hash Agg 用掉的内存,是不是要等到事务结束才释放?
  • 如果 SQL 很慢,是不是这些内存会一直挂着不放?
  • spill 到磁盘,是不是说明 PostgreSQL 内存管理不行?

如果你也有过这些疑问,那么这篇文章想讲清楚一件事:

PostgreSQL 里和 work_mem 相关的内存,绝大多数情况下并不会等到事务结束才释放。很多内存其实会在 SQL 执行过程中就被提前回收。

而且,排序、Hash Join、Hash Aggregate 这三类算子的释放方式还不完全一样。

这也是为什么很多人明明配了很大的 work_mem,却仍然搞不清楚 PostgreSQL 的真实内存峰值到底从哪来。

先说结论: work_mem 不是“申请了就一直占着不放”

一句话先说透:

work_mem 不是预分配内存,而是排序、哈希等执行操作的一个软预算。

也就是说:

  • 不是一上来就把这块内存全申请出来
  • 不需要就不会真的分配那么多
  • 超了也不一定报错,很多时候会转而 spill 到磁盘
  • 更关键的是,这些内存并不会一直撑到事务结束

更准确的规律其实是:

  • 有些内存会在执行过程中就提前释放
  • 有些会在执行节点结束时释放
  • 最晚通常在 SQL 语句结束时,由 executor 统一回收
  • 一般不会等到事务结束,更不会等到连接断开

如果你以前理解成“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 更大。

Sort: 排序不是最后才释放, 但也不是所有内存都会立刻释放

先看排序。

排序主要由 tuplesort.c 管理。它会创建几层 memory context,用来保存排序过程中的元数据、tuple 数据、merge 状态等。

但真正重要的不是“创建了几个 context”,而是:

排序过程中确实存在不少提前释放动作,但不是所有排序相关内存都会立刻释放。

比如:

  • 一批内存中的 tuples 排完并写入临时文件后,会 reset 这批 tuple 对应的工作内存
  • 进入 merge 阶段时,会释放不再需要的数组和缓存
  • merge 过程中,某个输入 tape 用尽后,会立即关闭,释放读缓冲
  • 如果是 bounded sort,某些注定进不了最终结果的 tuple,会当场释放

也就是说,排序并不是“先攒一堆,最后统一释放”。

它更像是:

一边处理,一边把已经没用的那部分工作内存尽快回收。

但这里有个容易忽略的细节:

Sort 的释放并不总是“立刻彻底释放”。

tuplesort 内部有 maincontextsortcontext 之分。 执行过程中经常被 reset 的,主要是 sortcontext 以及它下面的 tuple 工作内存; 而 maincontext 里保存的 Tuplesortstate 本体、排序键元数据等,可以跨多个 batch 复用。

这在 Incremental Sort 里尤其明显。增量排序会反复调用 tuplesort_reset() 来清理当前 batch 的排序工作区,但 maincontext 仍然保留,以便下一批继续复用。真正彻底释放,要等到节点结束时调用 tuplesort_end()

所以对于 Sort 来说,更准确的说法应该是:

大量排序工作内存会在执行过程中提前回收,但部分排序元数据和状态可能会保留到节点结束。

Hash Join: 不是逐条释放, 而是按 batch 成批回收

再看 Hash Join。

Hash Join 的思路和 Sort 不太一样,它不是那种“经常细粒度释放”的风格,而更偏向于:

按 batch 批量处理,按 batch 批量释放。

它内部大致会有几类内存:

  • 整个 join 生命周期都要用的
  • 当前 batch 才需要的
  • spill 到磁盘时使用的缓冲

Hash Join 最重要的释放动作是:

每处理完一个 batch,就 reset 这个 batch 的内存 context。

这意味着:

  • 这一批 tuple 的内存会被一次性回收
  • 不需要逐条 pfree
  • 回收成本更低,也更符合哈希构建阶段的使用模式

如果内存继续上涨怎么办?

PostgreSQL 的策略不是“硬扛”,而是:

  • 增加 batch 数
  • 把部分数据写到磁盘
  • 降低当前内存压力

这就是为什么 Hash Join 在内存不够时,往往表现为“分批写盘”,而不是简单失控。

另外,Hash Join 在某些路径下也会做更早的回收,比如:

  • batch 切换时 reset 整个 batchCxt
  • skew bucket 占用过高时提前清理
  • batch 数增大时,把不属于当前 batch 的 tuple 迁移到磁盘并释放对应内存块

换句话说,Hash Join 的内存管理核心不是“精细到每条 tuple 都单独释放”,而是:

用 batch、spill 和批量 reset,把整体内存控制在一个可收敛的范围内。

Hash Agg: 回收很积极, 但不要简单理解成“每个 group 完成就立刻释放”

如果说 Sort 是“边做边释放”,Hash Join 是“按 batch 释放”,那么 Hash Agg 可以说是:

释放粒度很多、回收动作很积极的一类。

但这里有一个很重要的纠偏点:

不能简单说成 Hash Aggregate 每完成一个 group,就会把这个 group 的聚合状态立刻释放。

更准确地说,聚合相关清理有几种不同粒度:

  • 每处理一条输入 tuple,会 reset tmpcontext,释放这条 tuple 的临时表达式计算内存
  • 对于 sorted/plain aggregation,在 group 边界会对相应 context 做 ReScanExprContext(),释放已完成 group 的状态
  • 对于 HashAgg,更关键的是在 spill batch 切换时重置哈希工作集,而不是“每输出一个 group 就立即释放整组状态”
  • 节点结束时,ExecEndAgg() 才会做最终清理

如果内存超限,Hash Agg 会进入 spill 模式:

  • 不再继续无限扩展内存哈希表
  • 新 group 的 tuples 直接写入磁盘
  • 后续再按 batch 读回处理

在处理每个 spilled batch 前,还会重置相关 hash 工作内存,让下一批重新获得预算空间。

所以更准确的说法应该是:

Hash Agg 的内存回收非常积极,但它的主要释放粒度是 per-tuple、spill batch 切换和节点结束,而不是简单概括成“每个 group 完成就立刻释放”。

真正应该怎么理解 work_mem

看到这里,其实就能把 work_mem 的概念纠正过来了。

work_mem 不是:

  • 一个连接固定拥有的内存
  • 一个 SQL 只能使用一次的内存
  • 一块申请后直到事务结束才释放的内存

而更应该理解为:

执行器里某些排序/哈希类操作可用的软预算,实际内存会随着执行过程动态分配、动态回收,超限后再通过 spill 转移到磁盘。

所以,调优 work_mem 这件事,重点从来都不只是“值设多少”。

更重要的是理解:

  • SQL 里到底有多少个会消耗 work_mem 的排序或哈希操作
  • 是 Sort 多,还是 Hash 多
  • 有没有并行
  • 会不会 spill
  • 内存峰值到底是节点叠加造成的,还是单个节点失控造成的

为什么这个问题在生产上特别重要

因为现实里很多 PostgreSQL 的内存问题,并不是“不会释放”,而是“你低估了会同时申请预算的节点数量”。

举个非常常见的误区:

有人看到磁盘 spill,第一反应就是:

work_mem 太小了,调大。”

但问题是,如果一条复杂 SQL 本来就有很多排序和哈希操作,你把 work_mem 从 64MB 调到 256MB,单个操作确实更不容易 spill 了,但整体内存峰值可能反而暴涨。

结果就是:

  • spill 变少了
  • 但数据库整体内存压力更高了
  • 并发一上来,系统反而更危险

所以 spill 不是绝对的坏事。 很多时候,适度 spill 其实是 PostgreSQL 在“内存安全”和“执行效率”之间做的合理平衡。

小结

最后把这件事收成一句话:

PostgreSQL 中 work_mem 相关内存,并不是等到事务结束才释放,而是大量内存在执行过程中就会按不同粒度被提前回收。

但也不要把这件事理解得过于绝对:

  • Sort 会在 run dump、merge、tuple 淘汰、tape 用尽等阶段及时回收,但部分排序元数据和状态可能保留到节点结束,Incremental Sort 就是典型例子
  • Hash Join 主要通过 batch reset、分批写盘、chunk 驱逐来控制内存
  • Hash Agg 会在 tuple 级、spill batch 级和节点结束等多个层次释放和重建工作集,但不能简单理解成“每个 group 完成就立即释放整组状态”

所以,理解 PostgreSQL work_mem 的关键,不是只记住参数值,而是理解它背后的执行器行为。

只有这样,你才能真正看懂:

  • 为什么有的 SQL 明明很慢,但内存并没有一直涨
  • 为什么有的 SQL 看起来不复杂,却能吃掉很多内存
  • 为什么 spill 不一定是坏事
  • 为什么 work_mem 不能简单粗暴地无限调大

如果你之前也一直把 work_mem 理解成“事务级保留内存”,那现在可以把这个旧印象彻底改掉了。

参考

本文基于 PostgreSQL 源码阅读与 DeepWiki 辅助分析整理。

参考链接: https://deepwiki.com/search/postgresql-workmem-sql_281d8b2c-c9af-4594-b743-6ff484c6bbab

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 digoal德哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • PG 的 work_mem 到底什么时候释放? 很多人理解错了
  • 先说结论: work_mem 不是“申请了就一直占着不放”
  • 为什么很多人会误解 work_mem
  • Sort: 排序不是最后才释放, 但也不是所有内存都会立刻释放
  • Hash Join: 不是逐条释放, 而是按 batch 成批回收
  • Hash Agg: 回收很积极, 但不要简单理解成“每个 group 完成就立刻释放”
  • 真正应该怎么理解 work_mem
  • 为什么这个问题在生产上特别重要
  • 小结
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档