PostgreSQL autovacuum 与 freeze 实测:长事务卡住 30 万 dead tuples,VACUUM FREEZE 把 relfrozenxid 推进 7
在 PostgreSQL 17.11 测试库(events 200 万行)实测:idle in transaction 快照会让 VACUUM 报出 300000 are dead but not yet removable;长事务结束后一次清掉;VACUUM (FREEZE) 把 rel_age 从 7 推到 0,冻结 24693 个页、200 万元组;表级调低 scale_factor 后约 14 秒触发 autovacuum。附可复跑脚本、只读巡检 SQL、样例 CSV。不做危险的 XID 耗尽实验。
PostgreSQL 17.11 实测(测试库、自造 200 万行):先 BEGIN 读一行再 UPDATE 30 万行,此时 VACUUM VERBOSE 写着「300000 are dead but not yet removable」,n_dead_tup=300000(19.11%);长事务一提交,再 VACUUM 一次就清成 0。VACUUM (FREEZE) 把 relfrozenxid 从 4770 推到 4777,rel_age 7→0,冻结 24693 页(71.43%)。autovacuum_freeze_max_age 默认 2 亿,本库 xid_headroom 约 1.99996 亿——余量很大,但长事务照样能把清理和 freeze 往后拖。
适用对象
- 长事务必须开在 UPDATE 之前,旧版本才会对它可见:本次先 BEGIN 再改 30 万行,VACUUM 明确写「300000 are dead but not yet removable」;事务结束后一次清掉。
- VACUUM (FREEZE) 会 aggressively 扫描并推进 relfrozenxid:本次 rel_age 7→0,frozen 24693 pages / 2000000 tuples,耗时 0.18 秒(193 MB 堆)。
- 把表级 autovacuum_vacuum_scale_factor 调到 0.02、threshold=1000 后,UPDATE 10 万行约 14 秒就触发了 autovacuum(autovacuum_count 1→2,n_dead 100000→0)。
- wraparound 保护相关:autovacuum_freeze_max_age=2e8,本测试库 db_age≈4049,xid_headroom≈199995951。本文不人为耗尽 XID;生产先看余量和长事务,再谈紧急 freeze。
先看四个和 freeze 有关的数字
本机默认:autovacuum=on;autovacuum_freeze_max_age=200000000(表年龄超过它会强制 anti-wraparound vacuum);vacuum_freeze_table_age=150000000(普通 VACUUM 何时更积极地推进 relfrozenxid);vacuum_freeze_min_age=50000000(行至少这么老才冻);vacuum_failsafe_age=1600000000(紧急时跳过部分延迟)。
当前库 age(datfrozenxid)≈4049,距离 2 亿还有约 199995951 的 xid_headroom。余量很大不代表可以忽视长事务——清理被挡时,表会先在 dead tuples 和膨胀上出问题。
| 参数 | 值 | 含义(简述) |
|---|---|---|
| autovacuum_freeze_max_age | 200000000 | 超过后强制 anti-wraparound autovacuum |
| vacuum_freeze_table_age | 150000000 | VACUUM 更积极推进 relfrozenxid 的阈值 |
| vacuum_freeze_min_age | 50000000 | 行 xmin 至少这么老才会被冻结 |
| vacuum_failsafe_age | 1600000000 | 紧急跳过部分延迟以尽快冻结 |
长事务挡住 vacuum:300000 dead but not yet removable
正确顺序很重要。会话 L:BEGIN; SELECT id,xmin FROM events WHERE id=1; 然后 pg_sleep(25)。另开会话 UPDATE 30 万行。此时 L 的 backend_xmin 还钉着旧快照,那 30 万行的旧版本对 L 仍然可见。
立刻 VACUUM (VERBOSE) events,日志原文:tuples: 6 removed, … 300000 are dead but not yet removable。统计上 n_dead_tup=300000,dead_pct=19.11%。L 一 COMMIT,再 VACUUM 一次:tuples: 300000 removed,n_dead_tup=0。
我第一轮测反了:先 UPDATE 再 BEGIN。那时旧版本对后来的快照已经不可见,VACUUM 直接清掉了约 80 万行,VERBOSE 写「0 are dead but not yet removable」——看起来「长事务没影响」,其实是实验顺序错了。下载脚本里是纠正后的顺序。
本节判断
- idle in transaction 和「还在跑的长查询」一样钉住 xmin。巡检 SQL 第 4 段查 pg_stat_activity 里 xact_start 非空的会话。
- pg_stat_progress_vacuum 在本次一次较大的清理中抓到 phase=vacuuming indexes,heap_blks_total=34568。
| 时机 | VERBOSE 关键句 | n_dead_tup |
|---|---|---|
| 长事务持有快照时 | 300000 are dead but not yet removable | 300000(19.11%) |
| 长事务提交后 | 300000 removed,0 not yet removable | 0 |
VACUUM (FREEZE):rel_age 7 → 0,冻结 200 万元组
普通 VACUUM 清 dead tuples;带 FREEZE(或年龄到了 vacuum_freeze_table_age)会更积极地冻结行、推进 relfrozenxid。本次 FREEZE 前 relfrozenxid=4770、rel_age=7;之后 relfrozenxid=4777、rel_age=0。
VERBOSE:aggressively vacuuming;frozen: 24693 pages from table (71.43% of total) had 2000000 tuples frozen;elapsed 0.18 s。库级 datfrozenxid 没动(仍受其他对象牵制),表级年龄已经归零。
生产上不要把 FREEZE 当日常刷子——它会制造更多 WAL、扫更多页。优先保证 autovacuum 能跑、长事务能杀掉;年龄告警再针对性 FREEZE 大表。
调低阈值后,约 14 秒触发 autovacuum
把该表 autovacuum_vacuum_scale_factor 调到 0.02、threshold=1000(默认 scale_factor 0.2,大表更懒),再 UPDATE 10 万行。autovacuum_count 从 1 升到 2 大约等了 14 秒(每 2 秒探一次,第 7 次见到),n_dead_tup 从 100000 落到 0,last_autovacuum 记到 2026-10-26 10:23:04+08。
这不是建议你把所有表都调到 0.02,只是证明:autovacuum 在门槛满足且没有被长事务钉住时会自己来。调参前先看 pg_stat_user_tables 的 last_autovacuum 和 n_dead_tup,以及是否有 idle in transaction。
50 万 dead tuples 时 seq scan 慢多少
关掉该表 autovacuum,UPDATE 50 万行制造 dead tuples,强制 Parallel Seq Scan 做 COUNT(*) WHERE status=1:Execution Time 84.294 ms。VACUUM 清掉后再跑同一计划:70.256 ms。差别存在,但不是数量级——堆页面数没变,只是多扫了死元组。膨胀严重、页面真的变多时,差距会大得多;那是 bloat 问题,需要另测 pg_stat / pgstattuple。
两个脚本层面的坑
psql -c "VACUUM events; ALTER TABLE …" 会把多条语句放进一个隐式事务块,VACUUM 直接报 ERROR: VACUUM cannot run inside a transaction block。VACUUM / VACUUM FREEZE 必须单独一条命令。
用 pg_stat_user_tables.n_dead_tup 做「刚 UPDATE 完」的即时判断可能短暂不准;以 VACUUM VERBOSE 的 removed / not yet removable 为准。只读巡检 SQL(下载区)6 段覆盖参数、库年龄、表 Top30、长事务、progress、接近 freeze_max_age 一半的表。
觉得有用?转给正在做这件事的同事或朋友
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
PostgreSQL 长事务为什么会导致表清不掉死元组?
长事务的快照还需要旧行版本。本次先 BEGIN 再 UPDATE 30 万行,VACUUM VERBOSE 明确写「300000 are dead but not yet removable」;事务提交后再 VACUUM 才清成 0。
VACUUM 和 VACUUM FREEZE 有什么区别?
VACUUM 主要清 dead tuples;FREEZE 还会积极冻结行并推进 relfrozenxid。本次 FREEZE 把 rel_age 从 7 推到 0,冻结 24693 页、200 万元组。
autovacuum_freeze_max_age 是什么?到了会怎样?
默认 2 亿。表的 age(relfrozenxid) 超过它时,autovacuum 会强制做 anti-wraparound vacuum。本次测试库余量约 1.99996 亿,未做到达实验。
怎么看现在有没有 vacuum 在跑?
查 pg_stat_progress_vacuum。本次抓到 phase=vacuuming indexes,heap_blks_total=34568。没有行就表示当前没有(你可见的)vacuum。
可以把 autovacuum 关了改成夜间手动 VACUUM 吗?
可以,但长事务和白天的 dead tuples 会一直堆到夜里;freeze 滞后还会逼近 wraparound 保护。更稳的是保留 autovacuum,用 idle_in_transaction_session_timeout 限制闲置事务,再对大表微调阈值。
下一步
推荐动作
你提供只读巡检 SQL 的结果(可脱敏),我们标出 dead 比例高、rel_age 高、以及钉住 xmin 的长事务,给出清理与 freeze 的优先顺序;维护动作在你方窗口执行。