单个 cursor 本身消耗很小,真正吃内存的是它拖住的共享缓冲、临时文件、以及解析 / 执行计划相关的进程私有内存。Cursor 是 “内存放大器”,而不是 “内存大胃王”。下面分层拆解。
一、总账:这些内存从哪来
| 内存层 | 归属 | Cursor 相关消耗 |
|---|---|---|
| 共享内存(Shared Buffers + WAL + 锁) | 整个实例 | Cursor 打开期间钉住的缓冲页、tuple 快照 / 可见性 |
| 进程私有内存(backend process) | 每个连接进程 | 解析树、计划树、执行状态(大头) |
| 临时文件(temp_files) | 磁盘 / OS page cache | 排序、Hash Join、物化超过 work_mem 时溢出 |
| OS 层 | 每个 backend 进程 | 进程堆(C 内存分配器 arena)、栈 |
Cursor 打开的瞬间几乎不分配共享内存,主要堆在 backend 进程的私有内存里。所以 “cursor 太多 → 内存涨”,涨的是每个连接进程的 RSS。
二、每个 Cursor 具体消耗什么(按占用从大到小)
1. 执行器状态(ExecutorState)—— 最大头
打开 cursor 会建立完整的执行状态:
- Portal:cursor 的外壳(含查询字符串、计划、tuplestore)。
- ExprContext:表达式计算上下文,包含内存上下文(MemoryContext)。
- TupleTableSlot:每一行都要落地到 slot,绑定在内存上下文里。
- 内存上下文(MemoryContext):PG 的核心。每个 portal 绑定一个
PortalContext,下面又挂ExecutorState、ExprContext、PerTuple等子上下文。这些上下文各自有内存池(malloc arena),即使内容释放了,已申请的堆块也可能不还给 OS(PG 内存上下文块按需增长、极少 shrink)。
关键点:内存上下文不会主动收缩。一个 cursor 曾处理过大量行,申请过的内存块会一直保留在进程堆里,直到该 portal 关闭并清理上下文。所以 “打开过” 比 “正在用” 更占内存。
2. 计划树 + 解析树
打开 cursor 会走完整的 parse → analyze → plan:
- 语法树、查询树(Query)、计划树(Plan)全部驻留内存。
- 含参数化查询时,
Bind阶段把参数值、计划参数拷进ParamListInfo。 - 一次 prepare + 多次 execute 的缓存计划(CachedPlanSource)也占内存,且是常驻的。
3. 排序 / Hash 的私有工作区
游标里若有 ORDER BY / GROUP BY / DISTINCT / JOIN:
- 未超过
work_mem(默认 4MB):全在进程私有内存里做tuplesort/hashjoin。 - 超过
work_mem:溢出到临时文件,此时内存不涨但磁盘 IO 和 OS page cache 上涨 —— 这也是 “内存涨” 的另一种形态(OS 缓存被 tmp 文件吃)。
4. 事务快照(Snapshot)
每个 cursor 打开都会登记一个快照(取决于隔离级别 /WITH HOLD):
- 快照本身小,但它让旧版本行(dead tuple)不能立即被 VACUUM 清理,导致表膨胀 → 共享缓冲被陈旧数据占满,间接推高共享内存占用。
5. Cursor 声明对象本体
cursor name、描述符等:极小,可忽略。
三、致命差异:两种 Cursor 完全不是一回事
| 类型 | 生命周期 | 内存去向 |
|---|---|---|
| 事务内游标(默认) | 随事务结束释放 | 释放相对及时,事务提交后 portal 清理 |
WITH HOLD 游标 | 跨事务存活,直到显式 CLOSE | 常驻进程内存,不关闭就不释放 → 这才是 “越开越多、内存只涨不跌” 的真凶 |
大量
WITH HOLDcursor 同时打开 = 每个都锁一份计划 + 执行状态 + 快照,且各自的内存上下文块持续累积,这是生产环境内存飙升最常见的病根。
四、怎么证实 / 怎么排查
-- 1. 看每个连接进程到底开多少游标
SELECT pid, count(*)
FROM pg_cursors
GROUP BY pid;
-- 2. 直接量:每个 backend 的私有内存(Linux)
SELECT pid, application_name,
pg_size_pretty(pg_stat_get_backend_memory(...)) -- 需扩展
FROM pg_stat_activity;
-- 或 OS 侧
SELECT pid, pg_size_pretty(rss) FROM pg_stat_activity; -- 参考 pg_stat_activity 内存列
OS 验证(每个 backend 的 RSS 与游标数量正相关):ps -o pid,rss,cmd -p <pid>,对比关闭游标前后的 RSS 差。
五、结论一句话
Cursor 多 → 内存涨,涨的主要是每个 backend 进程私有的执行器状态 + 内存上下文堆块(且不主动归还)+ 被拖住的共享缓冲与临时文件缓存。其中
WITH HOLD游标 + 事务内快照是内存 “只升不降” 的两个放大器。

徐万新之路

