除旧布新:VACUUM、冻结与膨胀治理
28 除旧布新:VACUUM、冻结与膨胀治理
PostgreSQL 的写入并不在提交时把旧世界擦掉。
UPDATE 写出新行版本,DELETE 标记旧版本失效;旧版本必须继续存在,直到所有可能看见
它的快照都离开。这个选择换来了读写并发,也把空间、统计、事务年龄和索引维护变成一套
持续运行的生命周期:
这条链上任一环被阻断,表现都可能是“膨胀”,但处理方法完全不同:
所以本章不把 VACUUM 当作一个“清垃圾命令”,而把它放回四条相互关联的控制回路:
| 控制回路 | 主要问题 | 关键证据 |
|---|---|---|
| MVCC 回收 | 哪些旧版本已经无人可见 | backend_xmin、pgstattuple、dead tuple |
| 空间与访问 | 空间能否复用,VM/FSM 是否更新 | relation size、FSM、VM、HOT |
| 事务年龄 | 离回卷保护线还有多远 | relfrozenxid、datfrozenxid、relminmxid |
| 结构与生命周期 | 是否需要重建或整片退役 | amcheck、pg_index、partition manifest |
三个必须分开的结论
“不可见”不等于“已经移除”
一条旧版本对当前事务不可见,不代表它对所有事务不可见。决定是否可回收的是全局清理 边界,而不是当前查询的快照。
“已经回收”不等于“文件已经缩小”
普通 VACUUM 主要把空间登记为关系内部可复用。它在特定条件下可能截断关系尾部,但
不承诺把散落在文件中间的空洞交还操作系统。VACUUM FULL 会重写表并取得
ACCESS EXCLUSIVE 锁,不能作为日常扫尾。
“dead tuple 很多”不等于“表已经异常膨胀”
n_dead_tup 是累计统计系统的估计;一次写入尖峰、健康的稳态 churn、被长事务阻断、
真正的 heap bloat,可能给出相似的瞬时数字。至少要联合:
再决定“等下一轮、手工 VACUUM、解除保留者、在线重建,还是安排离线重写”。
本章实验:让旧快照亲自阻断回收
正式实验在已确认的 Pigsty pg-test 沙箱创建一次性数据库:
夹具初始有 60,000 行。实验先开启一个 REPEATABLE READ 事务并确认它持有非空
backend_xmin,然后:
结果:
| 时点 | 当前行 | pgstattuple dead tuple |
heap bytes | FSM 可用空间 |
|---|---|---|---|---|
| 初始 | 60,000 | 0 | 61,440,000 | 19,680,000 |
| churn 后 | 50,000 | 50,000 | 87,040,000 | 27,877,376 |
| 旧快照仍在,普通 VACUUM 后 | 50,000 | 50,000 | 87,040,000 | 17,480,000 |
精确释放旧快照并 VACUUM FREEZE 后 |
50,000 | 0 | 87,040,000 | 51,920,000 |
这个结果同时证明两件事:
- 扫过并不等于能清;旧快照仍需要那些版本时,普通
VACUUM必须保留它们; - 回收并不等于缩文件;最后 dead tuple 为零、FSM 可用约 51.9 MB,heap 文件仍是 87.04 MB。
最后一次维护还得到:
注意:这不是在声称普通 VACUUM 永远不会截断文件。本夹具只观察到“文件未缩、空间
可复用”;生产结论必须保留普通 vacuum 有条件截断尾部的例外。
同一条证据链中的索引与分区
实验没有在 heap 回收后停止。
完整性与重建
被重建的二级索引从 3,227,648 bytes 降到 1,589,248 bytes;relfilenode 改变,
索引仍只有一个、indisready/indisvalid/indislive 全为真。这个数据只说明夹具中的
重建完成,不能外推生产窗口的耗时、锁等待或空间余量。
分区退役
10,000 行过期分区采用:
导出文件为 547,894 bytes。只有在回灌后的 10,000 行逻辑摘要完全一致后,实验才删除 独立分区;父表保留 5,000 行当前数据。
公开结果见 maintenance-run.json,安全边界见
lab-contract.md。
本章学习成果
完成本章后,你应该能:
- 从 MVCC 快照解释
UPDATE、DELETE为什么留下旧版本; - 区分 tuple visibility、deadness、removability 与 reusable space;
- 解释 page pruning、HOT、regular vacuum、aggressive vacuum 的分工;
- 用 FSM、VM、relation size 和 physical tuple evidence 分别回答不同问题;
- 正确计算 PostgreSQL 18 的 autovacuum update/delete 与 insert 触发阈值;
- 用表级 storage parameter 做定点治理,同时避免把关闭 autovacuum 当调优;
- 从 worker、cost delay、memory、I/O 和 workload 联合判断维护竞争;
- 读取
pg_stat_progress_vacuum,但不把块比例冒充 ETA; - 从
backend_xmin、复制槽xmin/catalog_xmin、两阶段事务找出保留者; - 监控
relfrozenxid/datfrozenxid和relminmxid,避免只看数据库总年龄; - 在回卷紧急态按安装版本的官方流程解除保留者并让普通
VACUUM完成; - 区分 stable-state bloat、transient churn、index bloat 和 statistics error;
- 评估
VACUUM FULL、在线重写和并发索引重建的锁、空间、WAL 与失败残留; - 用
DETACH ... CONCURRENTLY、归档清单和回灌验证完成分区退役; - 用
amcheck建立分层完整性检查,而不把它当页校验或恢复演练的替代品; - 把 Pigsty 历史指标、日志和 dashboard 与 PostgreSQL 原生视图交叉验证;
- 输出日常、每周、每月和事件驱动的维护清单;
- 在过载与疑似损坏时分别安全路由到第 34、35 章。
本章目录
28.1 死元组与可见性
28.2 autovacuum 的触发与资源
28.3 冻结、XID 与保留者
- 28.3.1 XID 年龄、冻结与回卷保护
- 28.3.2 长事务、
backend_xmin与 idle in transaction - 28.3.3 复制槽
xmin与孤儿pg_prepared_xacts - 28.3.4 紧急态先解除保留并让 VACUUM 完成
28.4 膨胀与重建
28.5 分区生命周期
- 28.5.1 新分区预建、约束和父表显式 ANALYZE
- 28.5.2
DETACH、归档、验证后删除 - 28.5.3 用分区退役替代大批量
DELETE - 28.5.4 将 ch04/ch07/ch11/ch16/ch28 串成能力索引
28.6 amcheck 与例行完整性检查
28.7 实战:建立维护节奏
- 28.7.1 制造膨胀、长事务与分区到期
- 28.7.2 从指标与原生视图判定维护优先级
- 28.7.3 执行清理、检查和分区退役并验证副作用
- 28.7.4 输出维护清单及 ch34/ch35 的安全路由
阅读路线
应用开发者:
重点是事务生命周期、长事务边界、表级写入特征和分区保留策略。
平台工程师:
重点是触发、资源、回卷安全、重建窗口和完整性检查。
两条路线必须合流:应用定义事务与数据生命周期,平台维护全局清理边界和资源预算。 应用留下无限事务,平台无法“调快 VACUUM”;平台盲目重写,应用也无法获得可预测服务。
版本与证据权威
本章命令以 PostgreSQL 18 为基线,正式 run 使用 18.6;平台示例以 Pigsty 4.5 为 参考实现。维护与紧急恢复语义应按实际安装 major/minor 的 PostgreSQL 官方文档 执行,不把旧版本博客或平台二次说明覆盖到新版本。
特别是事务 ID 即将耗尽的处置,PostgreSQL 18 官方流程要求先处理 prepared xact、
长事务和旧复制槽,再运行普通 VACUUM;它明确不建议在该状态使用
VACUUM FULL 或 VACUUM FREEZE,一般也不需要 single-user mode。本章 28.3.4
按这个版本事实展开。
核心资料:
- PostgreSQL 18:Routine Vacuuming
- PostgreSQL 18:VACUUM
- PostgreSQL 18:Vacuuming Configuration
- PostgreSQL 18:VACUUM Progress
- PostgreSQL 18:Visibility Map
- PostgreSQL 18:Heap-Only Tuples
- PostgreSQL 18:Table Partitioning
- PostgreSQL 18:amcheck
- Pigsty:PostgreSQL Monitoring
- Pigsty:PGSQL Dashboards
- Pigsty:
pig pgMaintenance Commands
实验文件
执行:
all 会按同一顺序执行。exercise 会产生真实 I/O、WAL、锁和短暂维护负载,只能在
已确认的一次性开发/测试环境运行。
本章验收
只有当你能交付以下证据,才算掌握本章:
“我跑了 VACUUM,没有报错”不是验收。
上一章:精益求精:参数调优与资源治理 · 返回下卷导读 · 下一章:移花接木:逻辑复制、迁移与异构同步 · 查看全书目录 · 查看索引中心
28.1 死元组与可见性
理解 VACUUM 的第一步,是放弃“表里只有当前行”的直觉。
PostgreSQL heap 存放的是行版本。一个逻辑主键在不同时间可能对应多条物理 tuple; 每个查询再用自己的 snapshot 判断哪一条可见。空间回收不能问:
这条旧版本对我还可见吗?
而要问:
集群清理边界之前,是否还存在任何合法快照可能看见它?
这两个问题之间的时间差,就是 MVCC 的空间债。
28.1.1 UPDATE/DELETE 如何产生旧版本
UPDATE 不是原地覆盖
概念上,一次更新经历:
事务提交后:
- 新快照通常看新版本;
- 更新前已经建立的旧快照仍可能看旧版本;
- rollback 则让更新产生的新版本不可见;
- vacuum 不能在旧快照离开前移除它仍可能访问的版本。
DELETE 不需要创建“空的新行”,而是在旧版本上记录删除事务;它同样要等到删除前的
快照离开,才可物理回收。
这就是 PostgreSQL 18 官方维护文档强调的边界:UPDATE/DELETE 不立即移除旧行,
因为它可能仍对并发事务可见。参见
Routine Vacuuming。
四个不同状态
不要把以下词混成一个 dead:
| 状态 | 含义 | 能否立即物理移除 |
|---|---|---|
| 对当前 snapshot 不可见 | 本查询不应返回 | 未必 |
| 对所有可能 snapshot 都不可见 | 已跨过清理边界 | 通常可成为回收候选 |
| 已由 vacuum/prune 处理 | tuple/line pointer 已清理或重定向 | 页内空间可复用 |
| 文件系统已收回 | 关系文件缩小或重写完成 | 是另一项操作结果 |
例如:
在 T1 结束前,T3 看不见旧版本不等于旧版本可删。
xmin、xmax、ctid 是诊断入口,不是业务 API
在教学夹具可以观察:
但要保留三项边界:
- 普通 SQL 只返回当前 snapshot 可见的版本,不会自动展示完整版本链;
ctid会随 UPDATE、表重写和行移动变化,不能当持久业务键;xmin/xmax是内部事务标识,存在冻结、回卷和 multixact 语义,不能当无限增长的 业务版本号。
若要检查页面内部,需要 pageinspect 等更侵入的诊断工具;它们适合受控故障分析,
不适合高频全库扫描。
一个容易忽略的命令级快照
数据修改 CTE 的兄弟子语句共享同一个命令快照:
不要依赖 deleted 再处理已经被 updated 修改的同一行,也不要用该命令末尾对原表的
count(*) 证明提交后状态。第 28 章实验最初正是在这里被验收器拒绝:
删除确实发生了;同命令读仍使用旧 command snapshot。正确证据是把修改计数和提交后 状态拆成两个 SQL 命令。这一例子也说明:没有明确 snapshot,所谓“当前行数”并不完整。
n_dead_tup 是估计,不是验尸报告
常用视图:
n_dead_tup 来自累计统计系统,更新是最终一致的,且本来就是估计。它适合:
- 找趋势;
- 排优先级;
- 关联写入速率和维护时间;
- 发现“长期只增不降”的异常。
它不适合单独证明:
- 精确有多少物理旧版本;
- 多少版本已经可由 vacuum 移除;
- 表文件浪费了多少字节;
- 是否应该
VACUUM FULL。
受控诊断可补:
pgstattuple 会扫描关系,能给更直接的 tuple/free-space 证据,但它也消耗 I/O;大型
生产表要先评估窗口,可考虑 pgstattuple_approx 或抽样型 bloat estimate。所谓
“更精确”不是“零成本”。
谁决定“仍可能可见”
清理边界受多类对象影响:
因此 VACUUM 没清掉时,先找保留者,而不是先提高 vacuum worker。第 28.3 节会把
每一类对象拆开。
28.1.2 vacuum、prune、HOT 与可见性图
VACUUM 不是唯一清理旧版本的地方,也不是所有清理都做同一件事。
page pruning:局部、机会式
访问 heap page 时,如果页面上有可安全裁剪的版本链,PostgreSQL 可以做 page pruning:
它的作用域是当前页,不会:
- 扫全表;
- 清所有索引死条目;
- 更新全关系统计;
- 推进整个表的
relfrozenxid; - 替代周期性 vacuum。
因此出现:
并不神秘,可能是热点页被访问时发生了 pruning。
HOT:避免不必要的索引版本
PostgreSQL 18 的 HOT 条件是:
- 更新没有修改任何被普通索引引用的列;核心中的 summarizing index 例外是 BRIN;
- 原 tuple 所在页面有足够空间放新版本。
满足时:
- 新版本不需要给普通索引添加新 index tuple;
- 中间版本可由 page pruning 更便宜地移除;
- 索引仍通过原始 line pointer 沿 HOT chain 找到可见版本。
参见 Heap-Only Tuples。
HOT 不是 UPDATE 的固定属性。下面这些都会降低它:
监控:
实验表使用 fillfactor=70,更新 40,000 行时观察到:
这不是“70% fillfactor 应得到 37.5% HOT”的公式。它只是说明同样不改索引列的 update, 仍有 25,000 行因为页内空间条件转到新页。是否调整 fillfactor,要联合:
不能只追求 100% HOT。
普通 VACUUM 的四项工作
官方文档把日常 vacuum 目的分成:
- 回收或复用 UPDATE/DELETE 占用的空间;
- 更新 planner statistics;
- 更新 visibility map,帮助 index-only scan;
- 防止 XID/MXID 回卷。
一次命令不一定对每项做相同强度。例如:
语义不同。INDEX_CLEANUP OFF 在极端防回卷场景可减少工作,但若长期跳过,索引死条目
和 heap line pointer 会累积;PostgreSQL 18 还有 failsafe 机制可在危险年龄自动跳过
某些昂贵工作。不要把临时救险选项变成常规模板。
FSM:哪里还有可放新 tuple 的空间
每个 heap 和除 hash 外的 index relation 都有 Free Space Map。它按页记录可用空间的 近似信息,帮助 insert/update 找到可复用页。
FSM 回答的是:
关系内部哪些页有空间可供后续写入?
它不回答:
操作系统现在多了多少 free bytes?
官方结构说明见 Free Space Map。
VM:哪些页可以被安全跳过
heap relation 的 Visibility Map 每页两位:
| bit | 含义 | 主要用途 |
|---|---|---|
| all-visible | 页内 tuple 对所有事务可见,没有 tuple 需要 vacuum | index-only scan 可跳 heap visibility check |
| all-frozen | 页内 tuple 已冻结 | anti-wraparound vacuum 可跳过 |
VM 是保守结构:
修改页面会清位,只有 vacuum 置位。因此:
不能直接推出页面里一定有 dead tuple。
观察:
需要进一步一致性检查时:
非空结果意味着 VM 与 heap 的约束可能损坏,应停止普通维护、保全证据并进入第 35 章
的数据抢救流程,而不是“清空 VM 看看”。pg_truncate_visibility_map 是修复性、
超级用户操作,会迫使后续 vacuum 重建 VM,必须有明确故障证据和变更记录。
官方说明见
Visibility Map 与
pg_visibility。
一张图看职责
28.1.3 回收可重用空间不等于归还文件系统
普通 VACUUM 的 steady-state 目标
高 churn 表最健康的状态通常不是“每晚回到最小文件”,而是:
后续更新和插入复用 plateau 内的空闲页,文件不再无限增长。PostgreSQL 官方文档建议
用较频繁的普通 vacuum 维持稳态,避免把 VACUUM FULL 当周期任务。
普通 VACUUM 也可能截断尾部
两个绝对命题都错:
普通 vacuum 主要原地处理页面;若关系尾部形成连续空页且锁等条件允许,它可能截断尾部。 文件中间的空洞不能靠截尾交还操作系统,但仍可由关系复用。
因此正确表述是:
普通
VACUUM不承诺按 dead tuple 数缩小文件;其主要产物是可重用空间,并可能在 条件满足时截断空闲尾部。
用三类 size,不用一个数字
再联合:
这些值回答不同问题:
| 指标 | 回答 | 不回答 |
|---|---|---|
| heap bytes | 主 fork 当前文件规模 | 其中多少马上可移除 |
| index bytes | 全部索引文件规模 | 每个索引是否逻辑健康 |
| total bytes | heap + indexes + TOAST 等总体 | OS 会不会马上得到空间 |
free_space |
heap 扫描看到的自由空间 | 未来 workload 是否会复用 |
| FSM sum | allocator 已知的页内空间 | 精确物理空洞 |
| dead tuple | 旧版本数量/字节 | 是否被长快照保留 |
正式 run 的反直觉结果
表已经具备很大的内部复用空间,却没有缩小。这是普通 vacuum 的正常结果,不是失败。
更重要的是,第一次普通 vacuum 在旧 snapshot 存在时:
它确实工作了;只是清理边界不允许移除那些版本。第二次在释放保留者后:
“命令成功”与“达成预期回收”必须分别验收。
什么时候才需要把空间交还 OS
先回答:
若会:
- 保留稳定 plateau;
- 让 autovacuum 跟上;
- 调整 fillfactor/索引设计;
- 监控增长斜率。
若不会,且空间有现实价值:
才评估重写。
重写决策必须有预算
不同方法:
| 方法 | 主要效果 | 主要代价 |
|---|---|---|
normal VACUUM |
页内复用、VM/freeze | 不整理中间空洞 |
VACUUM FULL |
重写并缩 heap | ACCESS EXCLUSIVE、额外空间、WAL、长窗口 |
CLUSTER |
按索引重写排序 | 强锁、额外空间、后续不会自动保持 |
pg_repack |
较在线地重建 | extension、额外对象/空间、trigger/锁/失败治理 |
| logical copy/swap | 最大控制力 | 迁移与双写/切换复杂度 |
| partition detach | 整片退役 | 设计前提、DDL/依赖/归档流程 |
第 28.4 节展开前三类重建,第 28.5 节处理分区退役。
停止线
看到以下任一情况,不要继续“加大清理”:
前六项先补治理证据;最后一项转入 第 35 章:数据抢救与工程取证。
本节检查清单
你应能对一张表给出:
只有这些信息组合起来,VACUUM 是否健康、是否被阻断、是否需要重写才是可回答的问题。
延伸阅读
- PostgreSQL 18:Routine Vacuuming
- PostgreSQL 18:VACUUM
- PostgreSQL 18:Heap-Only Tuples
- PostgreSQL 18:Free Space Map
- PostgreSQL 18:Visibility Map
- PostgreSQL 18:
pgstattuple - PostgreSQL 18:
pg_visibility
28.2 autovacuum 的触发与资源
autovacuum 不是“每隔一分钟把所有表 vacuum 一遍”。
它是一套按数据库调度、按表判断资格、按 worker 执行、按 cost budget 限速的后台系统:
排障必须分开问:
- 表有没有达到触发条件?
- 有 worker 能接活吗?
- worker 启动后在做什么?
- 为什么扫完仍留下旧版本?
“把 scale factor 调小”最多回答第一个问题的一部分。
28.2.1 阈值、比例、插入触发与表级覆盖
UPDATE/DELETE 触发公式
PostgreSQL 18 对普通 vacuum 的变化量先计算为:
对应:
当 $T_{\max}\ge 0$ 时,最终阈值为:
当 autovacuum_vacuum_max_threshold=-1 时,表示禁用最大阈值,最终阈值就是
$T_{\text{raw}}$,不能把 -1 直接代入 min()。
当自上次 vacuum 以来被 UPDATE/DELETE 变旧的 tuple 估计数超过这个阈值,表取得 vacuum 资格。
PostgreSQL 18 引入/使用 autovacuum_vacuum_max_threshold 作为上限:
若表为 10 billion rows:
版本低于 PostgreSQL 18 时,不要照抄这个公式中的 max 项;先查对应 major 文档和
pg_settings 是否存在。
INSERT-only 也需要 vacuum
只插不删的表没有 dead tuple,却仍需要:
- 更新 visibility map;
- 让 index-only scan 受益;
- 冻结旧 XID;
- 降低以后 aggressive vacuum 的工作。
插入触发公式为:
对应:
这不是简单的:
它还乘以“未冻结页面比例”。当表逐步 all-frozen,insert-based 触发的 scale 部分也会 变化。
ANALYZE 有自己的阈值
变化量包括 insert/update/delete。vacuum 和 analyze 可能:
不要把 last_autovacuum 当 last_autoanalyze。
freeze 资格优先于普通变化量
当 relfrozenxid 年龄超过 autovacuum_freeze_max_age,系统会强制 vacuum,即使:
- 普通
autovacuumGUC 为 off; - 表级
autovacuum_enabled=false; - dead tuple 没达到普通阈值。
同理,multixact 有独立的:
因此“关 autovacuum”既不安全,也不能保证后台永远不出现 worker;防回卷维护是正确性 机制,不是可选性能功能。
reltuples 和 change count 都不是精确实时值
触发器依赖:
pg_class.reltuples估计;- cumulative statistics 的变化计数;
- 最近 vacuum/analyze 更新;
- stats flush 的最终一致性。
边界附近出现几秒或一轮调度差异是正常的。排障时先查看实际输入:
这段查询仍没处理表级覆盖,生产版需要把 reloptions 合并进来。
表级覆盖:治疗特殊表,不复制全局配置
高 churn 大表、append-only 表和小型 catalog-like 表,触发策略可能不同:
查看:
恢复继承全局值:
表级覆盖适合:
不适合:
第 28 章实验为了让手工 vacuum 不被后台抢跑,在一次性夹具表上临时设置
autovacuum_enabled=false;数据库清理后该设置随表消失。公开结果明确将其列为
fixture_table_autovacuum_enabled=false,不是生产建议。
计算后还要看时间
一个表达到阈值只表示“有资格”,不表示立刻开始。launcher 要轮询数据库,worker 要 可用,其他 relation 可能排在前面。
评估维护能力,应比较:
若每小时产生 500 million obsolete tuples,而可用 worker 每小时只能处理 300 million, 调低触发阈值只会更早开始积压,不能解决服务率不足。
28.2.2 worker、cost delay、I/O 与业务竞争
launcher、worker slot 和 worker 上限
PostgreSQL 18 需要同时理解:
autovacuum_worker_slots 在启动时为 worker 预留 backend slot;
autovacuum_max_workers 是可同时运行 worker 的上限。把后者设得高于前者没有效果。
launcher 尝试把工作分散到各数据库;有 $N$ 个数据库时,会试图约每
autovacuum_naptime / N 启动一个 worker。它不是每个数据库独立一套无限 worker。
查询:
worker 数只是并发上限
增加 worker 可能:
- 减少多个数据库/表的排队;
- 让更多表并发扫描;
- 同时增加 I/O、CPU、buffer churn;
- 放大
autovacuum_work_mem总预算; - 与 foreground query、checkpoint、backup、replay 竞争。
若瓶颈是单块磁盘,三个 worker 已把设备打满,再加三个只会提高 queue depth 和业务 tail latency。
先看:
memory 按 worker 放大
autovacuum_work_mem 控制每个 autovacuum worker 可用的 maintenance memory;设为
-1 时回退到 maintenance_work_mem。粗略预算:
这仍是上界近似,不是每个 worker 永远一次性占满。它用于预留最坏并发,而不是预测 RSS 精确值。
内存主要影响:
- 收集 dead item identifiers;
- index vacuum cycle 频率;
- maintenance 内部结构。
它不会让一个被旧 snapshot 保留的 tuple 突然可删。
PostgreSQL 18 的 pg_stat_progress_vacuum 暴露:
可以判断是否因为维护内存限制而反复做 index vacuum cycle。
cost delay 是 I/O 影响控制,不是带宽保证
vacuum 给页面操作累计抽象 cost:
达到 vacuum_cost_limit 后,sleep vacuum_cost_delay 再继续。
autovacuum 对应:
若 autovacuum cost limit 非 -1,PostgreSQL 会在并行 worker 之间按比例分配,使各
worker limit 合计不超过该值。这意味着:
worker 变多,不等于每个 worker 都拿到完整 limit。
另外:
- 手工
VACUUM的 cost delay 默认关闭,除非显式设置非零; - 持有关键锁的操作段不会照常 sleep;
- failsafe 触发后会停止 cost delay,并跳过非必要工作来优先防回卷;
- cost unit 不是 IOPS 或 MB/s,必须用 OS/Pigsty I/O 指标校准。
正式实验为了可靠抓取进度,只在 vacuum session 设置:
session 结束即回退;没有修改集群配置。10 ms 是教学限速,不是推荐生产值。官方文档 指出正常配置通常应使用很小的 delay,大延迟并不理想。
I/O 不是唯一竞争
vacuum 还会:
所以 “iowait 不高” 不能证明 vacuum 无影响。可能:
- 数据在 cache,竞争表现为 CPU 和 buffer churn;
- device 很快,竞争表现为 foreground tail;
- cloud storage queue 未映射为 host iowait;
- cost delay 让 worker 大量 sleep;
- checkpoint/backup 与 vacuum 交织。
维护优先级不是一刀切
可把对象分三层:
P0 不能为了降低业务 I/O 无限限速;P2 不应在业务峰值争抢资源。
28.2.3 进度、阻塞与“为什么没清掉”
先确认 worker 身份
防回卷 worker 的 query 文本会带 (to prevent wraparound)。它与普通 autovacuum 的
取消策略不同:冲突锁通常可中断普通 autovacuum,但防回卷 worker 不会被自动中断。
读取原生 progress
PostgreSQL 18 的主要 phase:
解释时注意:
heap_blks_total是开始扫描时的规模;- VM 跳过的块仍会计入 scanned 的推进;
heap_blks_vacuumed可能跳跃;- index 可能有多个 cycle;
- truncation、锁等待和 index cleanup 的耗时不由 heap 扫描百分比线性预测。
所以:
是 scan progress,不是可靠 ETA。
“没清掉”的决策树
blocker inventory
再查:
不要在第一条查询里直接拼 pg_terminate_backend。先确认:
然后才能决定 cancel、terminate、commit、rollback 或 drop slot。
累计结果
PostgreSQL 18 增加/提供 vacuum/analyze 累计耗时列;部署跨版本查询时要先检查列存在。
累计 view 会 reset,必须联合 stats_reset 和 Pigsty 时序数据,不要把 reset 后的
“低计数”解释成改善。
Pigsty:历史趋势与原生瞬时事实互补
Pigsty 的监控栈把 PostgreSQL、PgBouncer、Patroni、主机和日志放到同一组
cls/ins/ip 标签下。对维护问题,常用:
| 页面 | 看什么 |
|---|---|
| PGSQL Tables / Table | dead/live、scan、vacuum、relation trend |
| PGCAT Table | 当前 catalog、size、bloat 类诊断 |
| PGSQL Persist | XID、WAL、checkpoint、archive、持久性 |
| PGSQL Activity / Session | backend、wait、长事务 |
| PGSQL Replication | slot、replica、replay/retention |
| PGCAT Locks | blocker/waiter |
| PGLOG | autovacuum verbose、warning、cancel/failsafe |
| NODE Instance | disk latency、queue、space、CPU、memory |
Dashboard 回答:
原生 SQL 回答:
两者必须互证。Grafana panel 不是另一个数据库真相层。
处置顺序
跳过第 4 步直接“手工再 vacuum 一次”,通常只会重复同一失败。
本节检查清单
延伸阅读
- PostgreSQL 18:The Autovacuum Daemon
- PostgreSQL 18:Vacuuming Configuration
- PostgreSQL 18:VACUUM Progress Reporting
- PostgreSQL 18:
pg_stat_progress_vacuum - Pigsty:PostgreSQL Monitoring
- Pigsty:PGSQL Dashboards
上一节:死元组与可见性 · 返回本章目录 · 下一节:冻结、XID 与保留者 · 查看全书目录 · 查看索引中心
28.3 冻结、XID 与保留者
空间膨胀会让系统越来越慢;事务 ID 回卷可能让系统为了保护数据而拒绝写入。
两者都由 VACUUM 参与治理,却不是同一个风险:
一张几乎不更新的静态表可能没有 dead tuple,却必须周期性冻结;一张高 churn 表可能 每天 vacuum,仍被一个旧 snapshot 阻止回收。成熟运维要同时看“垃圾速度”和“年龄 安全线”。
28.3.1 XID 年龄、冻结与回卷保护
XID 是全局 32 位循环空间
普通内部 xid 为 32 位,约每 42.9 亿次分配回卷一次。PostgreSQL 用模 $2^{32}$
比较:
若一个行版本保留超过约 20 亿个事务,它原本的插入 XID 会从“过去”落到“未来”的 比较区间。冻结的目的,是把已确定对所有当前与未来事务可见的老版本标成永久过去。
注意:
- XID 在事务第一次需要写入时才分配,不一定等于
BEGIN时间顺序; txid_current()等旧接口与xid8/epoch 语义不同;- 不能用 32 位
xmin做长期业务序列或跨回卷排序。
事务标识内部说明见 Transactions and Identifiers。
冻结是 flag,不是把 xmin 改成 2
PostgreSQL 9.4 以后,冻结通常设置 tuple header flag,并保留原 xmin 供取证;不能
因为查询还看到原 xmin 就断言“没冻结”。老版本升级数据库可能仍见
FrozenTransactionId=2。
正确证据是:
而不是:
关系、数据库和 TOAST 三层年龄
每个普通表/物化视图:
每个数据库:
database 值用于 cluster-level 预警,relation 值用于定位。表的 TOAST relation 也可能 最老,不能漏。
数据库总览:
当前数据库按关系下钻:
relfrozenxid 是最近一次成功推进该边界的 vacuum 结果,不是“最老一行的精确插入
时间”。age() 是相对于当前 XID 的事务数量,不是秒。
把年龄换成时间余量
同样 100 million age:
因此告警要看:
并给 maintenance duration、业务尖峰和失败重试留余量。仅用固定 age percentage, 无法表达突然增长的 XID burn rate。
获取近似分配速率可对固定间隔的 pg_current_xact_id()/监控计数做差,但读取函数是否
分配 XID、采样事务本身的影响要按接口语义处理。Pigsty 的 PGSQL Persist 历史曲线更
适合看持续速率,原生 catalog 负责当前边界。
regular、aggressive 和 failsafe
相关门槛:
PostgreSQL 会限制部分有效值,例如 vacuum_freeze_table_age 不会有效超过
autovacuum_freeze_max_age 的 95%。不要只读配置文件;读 pg_settings.setting
确认实际值。
eager freeze
PostgreSQL 18 的普通 vacuum 可能主动扫描一部分 all-visible 但未 all-frozen 的页面,
提前冻结,减少以后 aggressive vacuum 的工作;相关行为可用
vacuum_max_eager_freeze_failure_rate 调整。
这意味着:
不再等价于“绝不扫描可跳过页”,但它仍不保证每次全表 aggressive coverage。
实验中的年龄证据
第 28 章夹具不是回卷压力测试;它绝不消耗数十亿 XID。它只验证:
这些值证明 freeze 路径在一次性表上生效,不证明生产 threshold、maintenance duration 或 headroom 合理。
28.3.2 长事务、backend_xmin 与 idle in transaction
事务久不等于一定持有旧 snapshot,反之亦然
xact_start 告诉你事务开始时间;backend_xmin 告诉你该 backend 对 vacuum horizon
的贡献。它们相关,但不等价:
查询:
idle in transaction 为什么危险
应用执行:
backend 不用 CPU,却可能:
- 保留 snapshot,阻止 dead tuple 回收;
- 持有 relation/row/advisory locks;
- 占用 backend 与连接池槽;
- 让 DDL、vacuum、reindex 等等待;
- 让 table/index/WAL 间接增长。
所以“CPU 为 0”不是无害。
timeouts 要按角色与协议设计
PostgreSQL 18 提供:
其中:
idle_in_transaction_session_timeout专门终止在开放事务中 idle 过久的 session;transaction_timeout限制整个事务跨度;- 普通 idle session 不持有开放事务,危害与 idle-in-xact 不同;
- pool/middleware 可能无法优雅处理被服务端突然关闭的连接。
更稳妥的做法:
示例值不是通用推荐。应用必须:
- 正确 rollback/retry;
- 不在事务中等待用户输入或远程 API;
- 流式读取时理解 cursor/snapshot 生命周期;
- 为 migration、batch、backup 使用独立角色和窗口。
精确处置,不批量杀 idle
处置协议:
正式实验只终止:
结果:
没有使用“杀掉所有 idle in transaction”。
读副本也可能把 horizon 反馈到主库
hot_standby_feedback 可降低 standby query cancellation,但会把所需 xmin 反馈到
primary,导致 primary 保留 dead tuple。取舍是:
有 replication slot 时,还要联合 pg_replication_slots.xmin。只在 standby 查
pg_stat_activity 可能找不到 primary 膨胀的完整原因。
28.3.3 复制槽 xmin 与孤儿 pg_prepared_xacts
复制槽有两类保留线
pg_replication_slots 中:
| 字段 | 保留什么 |
|---|---|
xmin |
数据库必须保留的最老事务;更晚删除的 tuple 不能被 vacuum 移除 |
catalog_xmin |
逻辑解码所需系统目录 tuple |
restart_lsn |
consumer 仍可能需要的最老 WAL |
confirmed_flush_lsn |
逻辑 consumer 已确认接收的位置 |
wal_status/safe_wal_size |
WAL 保留状态与走向 lost 的余量 |
空间问题要分开:
“复制槽只会撑大 WAL”是错的。
查询:
inactive 不等于 orphan
一个 inactive slot 可能是:
- 正常短暂断线;
- 灾备系统等待窗口;
- CDC consumer 故障;
- 切换后遗留;
- 已废弃对象;
- standby 同步 slot。
drop slot 前必须确认:
官方文档明确提示:若删除仍会回来使用的 slot,对应 replica 可能需要重建。
因此:
不是发现 inactive 后的第一步。
prepared transaction 没有客户端也能继续持有状态
两阶段提交:
进入 prepared 后,它仍可持有:
- 已分配 XID;
- 行/表锁;
- 可见性和清理边界影响;
- 未决业务结果。
查询:
不要因为 pg_stat_activity 没有对应客户端,就判断“事务已消失”。
orphan 处置需要业务协调器事实
没有这些事实时,随意 ROLLBACK PREPARED 可能破坏跨系统一致性,随意
COMMIT PREPARED 也可能提交应回滚的业务。
正确 runbook:
若业务并不需要 2PC,应让 max_prepared_transactions=0 保持禁用,而不是启用后希望
“没人会忘”。
统一保留者清单
一张事故表至少包括:
| source | identifier | age | owner | needed by | action | proof |
|---|---|---|---|---|---|---|
| backend | PID/app | xmin age | team | transaction | commit/terminate | holder gone |
| slot | slot name | xmin/catalog age | CDC/DBA | consumer | resume/drop | consumer state |
| prepared | GID | xid age | coordinator | distributed tx | commit/rollback | business ledger |
只有 owner 和 action 明确,才进入执行。
28.3.4 紧急态先解除保留并让 VACUUM 完成
这一目的标题刻意没有写“立刻 VACUUM FREEZE”。
对 PostgreSQL 18,必须区分:
它们的正确动作不同。
第一阶段:预防与早期告警
正常系统应在远离危险线时:
在这个阶段,针对静态大表使用 VACUUM (FREEZE) 可以是有计划的维护动作:
前提是:
- 已评估 I/O、WAL、锁和 replica;
- 不是为了掩盖未知 blocker;
- 目标表与 TOAST 均被验收;
- 生产窗口已批准。
Pigsty 的:
封装的是 freeze vacuum;适合明确的计划动作,不应脱离 PostgreSQL 版本语义当作所有 XID 事故的万能按钮。
第二阶段:系统已接近或进入拒绝新 XID
PostgreSQL 18 官方文档说明:
- 临近回卷点会先产生必须 vacuum 的 warning;
- 剩余不足约 3 million XID 时,系统拒绝分配新 XID以保护数据;
- 已在运行的事务可继续,新的只读事务可启动;
- 普通
VACUUM仍可执行。
此时的优先顺序是:
官方文档对 PostgreSQL 18 还明确说:
原因是 hard-stop 状态要做恢复正常所需的最小工作:
VACUUM FULL自身需要/消耗 XID、强锁且重写;VACUUM FREEZE做超过最小恢复所需的工作;- single-user mode 会绕开保护并引入停机风险。
这与某些旧版本文章或旧 runbook 不同。执行时以安装版本官方文档为准。
“让冻结完成”的正确含义
在日常或 early warning 阶段:
不要为了降低 I/O 不断 cancel 防回卷 vacuum;解除 blocker,让 aggressive freeze 推进并验收年龄。
在已经拒绝 XID 的 hard-stop 阶段:
不要把
FREEZEoption 当口号;按 PostgreSQL 18 最小恢复流程先解除 prepared xact、长事务和旧 slot,再让普通VACUUM完成。
两者共同反对:
这些都没有安全地冻结旧 tuple,可能把正确性风险推向灾难。
紧急 runbook 的 fail-closed 门
ownership_confirmed=false 时,不能自动 drop slot 或 resolve prepared transaction;
需要事故指挥者和业务 owner 决策。
XID 与 MXID 要分案
MXID 用于多事务共同锁行,拥有独立:
XID 耗尽会阻断所有需要新 XID 的写;MXID 耗尽主要阻断需要创建新 multixact 的锁类 写入。事故名称、监控和 runbook 不能只写“wraparound”。
恢复写入不等于事故闭环
普通 vacuum 让系统重新接受写入,只是止血。根因可能仍是:
事故关闭条件:
否则下一次只是时间问题。
本节检查清单
延伸阅读
- PostgreSQL 18:Preventing Transaction ID Wraparound Failures
- PostgreSQL 18:Transactions and Identifiers
- PostgreSQL 18:Vacuuming / Freezing Configuration
- PostgreSQL 18:
pg_database - PostgreSQL 18:
pg_replication_slots - PostgreSQL 18:
pg_prepared_xacts - PostgreSQL 18:Client Timeouts
- Pigsty:
pig pg freeze
上一节:autovacuum 的触发与资源 · 返回本章目录 · 下一节:膨胀与重建 · 查看全书目录 · 查看索引中心
28.4 膨胀与重建
“膨胀”不是一个 catalog flag。
它是一个相对于预期有效载荷和未来复用的工程判断:
这几个减数没有一个能由 n_dead_tup 单独给出。重建又会产生锁、额外空间、WAL、
replica replay 和失败残留;误判膨胀,常常比接受一个稳定 plateau 更贵。
28.4.1 表膨胀、索引膨胀与统计误判
先分 heap、TOAST 和 index
这些层次不同:
“表 2 TB”必须说清是 heap、table size 还是 total size。
表膨胀的四个常见来源
对应动作:
| 来源 | 动作 |
|---|---|
| old snapshot/slot/2PC retains | 解除精确保留者 |
| vacuum service rate insufficient | 修触发/worker/I/O |
| retention permanently shrank | 评估 rewrite/partition |
| fillfactor/design overhead | 评估写放大与 scan tradeoff |
只有第三类天然指向“交还 OS”。
pgstattuple 更直接,但不是瞬时原子快照
返回:
它只拿 read lock 并逐页累计;并发写可在扫描期间发生,所以结果不是整张表同一时点的 原子 snapshot。大型表还会产生显著读取负载。
较轻量的候选:
它利用 VM 跳过部分 all-visible pages,换取估计。到底用哪一个要在 ticket 中声明 accuracy/cost。
index bloat 不是 heap dead ratio
B-tree 页面分裂、删除和 key distribution 会造成:
- 半空 leaf page;
- deleted/empty page;
- logical adjacency 与 physical layout 分离;
- 低 leaf density;
- 大量仍需维护但查询很少使用的 index tuple。
观察:
重点:
但仍不能写:
原因:
- index fillfactor 本来允许余量;
- 刚经历 page split;
- key 是随机、递增或时间窗口;
- 并发扫描期间数据在变化;
- 不同 access method 的空间模型不同;
- 低密度空间可能马上被写入复用。
还要看:
统计 reset 会影响 idx_scan;不能因为 reset 后为零就立即 drop index。
statistics error 会伪装成 bloat
常见误判:
先确认:
必要时:
然后再重算 estimate。
“大”与“膨胀”分开
一个 4 TB 表可能:
- live data 就是 4 TB;
- page density 合理;
- 维护跟上;
- query 通过 partition pruning;
- 无需重写。
一个 20 GB 表可能:
- live data 只有 1 GB;
- retention 永久下降;
- 文件系统只剩 5 GB;
- 重写却需要超过当前 free space;
- 已成为事故风险。
大小决定操作成本,浪费比例决定收益,headroom 决定可执行性。三者缺一不可。
诊断报告模板
报告结论应是:
而不是一个没有依据的 bloat_pct。
28.4.2 VACUUM FULL、在线重建与额外空间
VACUUM FULL 做了什么
PostgreSQL 18:
因此它:
- 需要
ACCESS EXCLUSIVE; - 比普通 vacuum 慢;
- 需要额外磁盘容纳新副本;
- 重写 heap,并重建相关索引;
- 产生大量 I/O/WAL;
- 可推高 replica replay 和 archive backlog;
- 使缓存重新变冷;
- 改变 tuple
ctid等物理标识。
命令:
语法简单,变更本身不简单。
为什么不能“磁盘快满时就 FULL”
VACUUM FULL 在完成前不释放旧副本;磁盘已经接近满时,它可能最缺执行所需空间。
预算至少覆盖:
不要用:
推算。可回收 500 GB 不表示先有 500 GB 可用。
锁窗口不仅是命令运行时间
若先无限等待锁,队列会在它后面形成:
生产执行要有:
示例值必须按对象预算;关键是 fail-fast 获取锁,而不是在峰值排队。
在线重写不是免费重写
pg_repack 一类工具通常通过:
把长时间强锁缩短到最终切换,但代价仍在:
- shadow copy 空间;
- 索引和 WAL;
- trigger/delta capture 开销;
- 长事务等待;
- extension/client/server 版本兼容;
- unique key/对象类型限制;
- DDL 并发限制;
- 中断后的临时对象和恢复。
Pigsty 提供:
它要求 pg_repack extension。--plan 只是先看计划,不是生产审批。执行前仍需:
Pigsty 默认扩展目录可提供 pg_repack 包与数据库 extension 映射,但实际是否启用要用
pg_extension 检查。
其他重写路径
| 路径 | 适合 | 主要风险 |
|---|---|---|
VACUUM FULL |
小表/可停写窗口/一次性大清理 | 长强锁 |
CLUSTER |
需要按索引重排且可停写 | 长强锁,物理顺序会再漂移 |
pg_repack |
需保持大部分读写 | 额外空间、delta、最终锁、扩展复杂度 |
| logical copy/swap | 迁移、类型/模型同时变更 | 双写/增量/切换验证 |
| partition detach | 过期数据整片淘汰 | 需预先按生命周期分区 |
“在线”应写成:
而不是一个布尔标签。
预执行 canary
对可复制数据:
canary 仍不能完全预测生产锁队列,但能排除明显容量和兼容性错误。
验收不只是“size 下降”
若 rewrite 改了 statistics,执行计划可能变化;第 7 章的 plan evidence 也应进入验收。
28.4.3 REINDEX CONCURRENTLY 的版本和失败处理
先问为什么重建
合理原因:
不充分:
索引疑似损坏时,先保存证据、确认 heap 和 backup,再决定重建;盲目重建可能覆盖关键 取证线索。
版本边界
REINDEX CONCURRENTLY 在 PostgreSQL 12 引入。本章以 PostgreSQL 18 语义为准:
跨版本自动化不能只看语法存在,还要检查:
- object kinds;
- partitioned relation 支持;
- progress view columns;
- exclusion/system catalog 限制;
- invalid-index recovery;
- minor release bug fixes。
普通与并发模式
普通:
会阻止 parent table 写入,并对索引本身拿强锁;planner 尝试锁表的各索引,所以读也可能 受到广泛影响。
并发:
允许正常 insert/update/delete 继续,但需要:
它做更多总工作、花更久、需要额外空间和 CPU/memory/I/O,并不“无锁”。
不能放在 transaction block
同一张表一次只能有一个 concurrent index build;并发 DDL 也受限制。自动化器要把 每个对象的状态作为可恢复 step,而不是把全库命令包成一个 transaction。
观察进度和等待
关键 phase 包括:
如果卡在 old snapshots,回到 28.3.2,不是再启动第二个 reindex。
失败后会留下 INVALID
官方文档明确:
_ccnew:新 transient index 未成功,应检查后 drop,再重试;_ccold:旧 index 在成功重建后未能 drop,通常应 drop 这个 old artifact;- 后缀可能带数字;
- INVALID index 不供查询,但仍可能带来 update overhead。
检查:
不要用:
作为通用 cleanup。先确认它的 indisready/valid/live、parent、constraint dependency
和本次 run marker;名称相似不是所有权证据。
约束索引需要额外验证
unique/primary/exclusion index 参与约束。并发重建会更新 constraint reference,但:
- exclusion constraint index 不能并发 reindex;
- unique violation 可让 build 失败;
- 不能只验 index name;
- 必须验 constraint 仍绑定正确有效 index。
正式实验
夹具的 churn_status_idx:
| 状态 | bytes | valid/ready/live | invalid artifact |
|---|---|---|---|
| before | 3,227,648 | true/true/true | 0 |
| after | 1,589,248 | true/true/true | 0 |
同时:
0.089 秒只对这一张小型、无并发业务的夹具成立。正式 run 明确不外推生产 duration。
reindex 也能影响 vacuum
并发 reindex 自身有多事务和 snapshot 等待。官方文档提醒:像任何长事务一样,它可能 影响其他表的 concurrent vacuum 清理边界。维护任务不能互相独立排程:
要在同一个资源与事务日历里编排。
生产完成条件
本节检查清单
延伸阅读
- PostgreSQL 18:Recovering Disk Space
- PostgreSQL 18:VACUUM
- PostgreSQL 18:REINDEX
- PostgreSQL 18:Routine Reindexing
- PostgreSQL 18:
pgstattuple - PostgreSQL 12 Release:REINDEX CONCURRENTLY
- Pigsty:
pig pg repack - Pigsty:Default Extensions
上一节:冻结、XID 与保留者 · 返回本章目录 · 下一节:分区生命周期 · 查看全书目录 · 查看索引中心
28.5 分区生命周期
如果数据天然按时间或租户整片到期,最好的 vacuum 往往是不要制造那些 dead tuple。
分区不是免费的性能开关;它是把数据生命周期编码进物理边界。只有 partition key、 retention unit、query pruning 和发布流程一致时,整片退役才成立。
28.5.1 新分区预建、约束和父表显式 ANALYZE
从生命周期单位反推边界
先定义:
再决定:
月分区不一定最好:
| 单位 | 优点 | 风险 |
|---|---|---|
| 日 | 退役粒度细 | partition 数、planning/catalog 开销 |
| 月 | 常见折中 | 大月仍可能过大 |
| 季/年 | 对象少 | 退役和维护粒度粗 |
| tenant hash/list | 隔离租户 | retention 可能仍需二级时间分区 |
目标不是最多 partition,而是让:
落在同一边界。
range 上界是排他的
边界:
共享的 2026-09-01 属于下一个 partition。若应用按本地日历月保留,必须明确 DST 和
timezone;不要让 session TimeZone 隐式决定 DDL literal。
预建,不等 insert error 报警
写入没有匹配 partition 会失败。生产应提前:
例如维护表:
pg_partition_tree() 适合多层结构:
离线装载后 ATTACH
大分区可先作为普通表准备:
若已有一个有效且与 partition bound 匹配的 CHECK constraint,PostgreSQL 可避免
在持有 partition ACCESS EXCLUSIVE 时扫描全表验证。attach 完成后,这个重复
constraint 可在评审后删除。
若 parent 有 default partition,还应给 default 添加排除新范围的 CHECK;否则 attach
可能扫描 default,且持有其强锁。
注意:
- expression partition key 有额外限制;
- list partition 是否接受
NULL影响 constraint; - subpartition 可能递归锁/扫到 leaf;
- parent 是 virtual structure,实际 index 在 leaf;
- attach 前要验证 index/constraint 与 parent 模板一致。
default partition 是缓冲区,不是垃圾桶
default 可避免未知 key 直接失败,但会带来:
若使用 default:
- 监控 row count;
- bad key 立即告警;
- 定期清空到正确 partition;
- 在 attach/detach runbook 中显式处理;
- 不把它当永久无限分区。
parent 必须显式 ANALYZE
partition leaf 的变化不会触发 parent auto-analyze;partitioned table 自身不直接存 tuple,
autovacuum 不会在 parent 上运行 ANALYZE。当首次装载或分布显著变化时:
parent-level statistics 会影响引用 partitioned table 的 plan。生命周期动作完成而漏掉 parent analyze,可能导致:
这不是物理 bloat,却常被误归因成“分区太多”。
分区模板是 schema release
创建脚本应来自同一 desired state:
不能靠:
就假设复制了所有业务语义。LIKE 的 INCLUDING ... 选项、partitioned parent 的虚拟
对象、trigger/RLS/publication 行为都要按版本验证。
28.5.2 DETACH、归档、验证后删除
detach 不是 drop
结果:
这是理想的 quarantine point:
普通与 CONCURRENTLY
普通 detach 对 parent 取得 ACCESS EXCLUSIVE。
PostgreSQL 18 的 concurrent 形式不是“零锁”,而是内部两个 transaction:
- 对 parent 和 partition 取得
SHARE UPDATE EXCLUSIVE,标记 pending detach 并提交; - 等所有使用 partitioned table 的旧 transaction 离开;
- 再对 parent 取
SHARE UPDATE EXCLUSIVE、对 partition 取ACCESS EXCLUSIVE; - 完成 detach,并给 standalone table 添加等价
CHECKconstraint。
限制:
中断后:
用于完成先前被取消/中断的 concurrent detach。自动化不能看到命令失败就直接重跑或
drop;先查 pending state,再决定 FINALIZE。
先关闭边界写入竞争
在 detach 前确认:
否则 detach 后:
- 新写入可能失败;
- 被路由到 default;
- 被误写到 archive standalone table;
- 数据清单在导出期间变化。
一种做法是先把旧 partition 业务状态标成 sealed,再等 maximum transaction duration 过去,最后 detach。真正的控制点在应用与数据产品,不只在 DDL。
归档清单至少有四层
- 对象清单
- 逻辑清单
- 归档文件清单
- 恢复清单
只有 file hash 相同,不能证明文件可以被当前工具恢复;只有 row count 相同,也不能证明 金额、范围和编码正确。
COPY 与 pg_dump 的选择
对于大型表,不要用:
在 server 端聚合整个数据集;第 28 章夹具只有 10,000 行,才用它作为教学逻辑摘要。 生产可用有序 chunk hash、COPY/Parquet manifest、业务聚合和独立 restore 合并证明。
正式实验的顺序
清理 validator 会拒绝:
删除后还要验 backup policy
partition 从在线库删除后:
- PITR 仍可在 retention window 内恢复历史 cluster;
- 逻辑 archive 负责更长期访问;
- backup retention 和 archive legal retention 可能不同;
- GDPR/删除义务也可能要求从 archive 到期清除;
- catalog/monitoring 应记录在线与归档位置的转换。
生命周期不是 DROP TABLE 结束,而是 ownership 从 online service 转到 archive service。
28.5.3 用分区退役替代大批量 DELETE
大 DELETE 的债
可能产生:
分批 delete 可控制 transaction:
但它仍逐行处理,且 ctid 只用于当次短事务。批处理适合选择性删除,不如整片
partition 退役。
detach 的收益来自事前设计
若 expired predicate 恰好覆盖完整 partition:
它避免制造海量 obsolete tuples。随后 drop standalone table 删除 relation files, 也不需要 vacuum 每一行。
但不能夸大为 $O(1)$、瞬间、无 WAL、无锁:
- catalog 要更新;
- parent/partition/FK 有锁;
- concurrent 形式要等旧 transaction;
- replica 要 replay DDL;
- drop 仍要处理 dependency 和文件;
- archive copy 仍按数据量花费 I/O;
- planning/catalog 对 partition 数敏感。
准确说:
对生命周期与 partition bound 对齐的数据,detach/drop 把逐行淘汰的核心成本转换成 受控 DDL 与归档成本。
何时不能替代
这时选择:
- 更合适的 partition key/subpartition;
- selective batch delete;
- logical archive+copy;
- tenant migration;
- schema redesign。
不要为了这次清理临时创建数千 partition;partitioning 是长期模型。
FK 与全局唯一性
partitioned table 的 unique/primary key 通常必须包含 partition key,才能由每个 leaf 的 局部索引共同保证全局逻辑唯一。跨 partition FK、引用 leaf、触发器和 publication 也会影响 detach。
设计前问:
若答案不清晰,retention 不是一个 DBA 单表任务。
DELETE 与 detach 的选择矩阵
| 条件 | batch DELETE | partition detach |
|---|---|---|
| 任意 predicate | 支持 | 不支持 |
| 整片时间范围 | 可做但昂贵 | 优先候选 |
| 在线逐步释放 | 支持 | 按 partition 粒度 |
| 归档后保留 standalone | 需 copy | 天然 |
| dead tuple/vacuum 债 | 有 | 不制造逐行债 |
| 前置建模 | 少 | 必须 |
| FK/依赖 | 行级处理 | DDL 级约束 |
28.5.4 将 ch04/ch07/ch11/ch16/ch28 串成能力索引
分区生命周期不是本章孤立技巧,而是贯穿设计、查询、发布和运维的能力。
第 4 章:数据表达决定边界
若时间语义错,partition boundary 再精确也会错删。
能力交付:
第 7 章:统计与 pruning 证明查询受益
第 7 章:执行计划与统计信息 负责:
能力交付:
分区多但查询不带 partition key,可能同时扫描大量 leaf;那不是 vacuum 能修的。
第 11 章:生命周期 DDL 是安全发布
第 11 章:模式变更与安全发布 负责:
能力交付:
DETACH CONCURRENTLY 仍是 production DDL。
第 16 章:时序/时空 workload 定义生命周期
能力交付:
只有 watermark 越过 partition end + late-arrival allowance,才可 seal/detach。
第 28 章:运营闭环
本章负责:
合起来:
可执行能力索引
| 能力 | 输入 | 证据 | 失败路由 |
|---|---|---|---|
| create future partition | calendar + schema | bound/tree diff | ch11 |
| attach loaded partition | valid CHECK + manifest | no scan/lock plan + row proof | ch11 |
| analyze hierarchy | distribution change | parent/leaf stats | ch07 |
| seal range | watermark + late window | no new writes | ch16 |
| detach | dependency + lock budget | topology diff | ch11/ch28 |
| archive | object + logical manifest | hash + restore | ch28/ch35 |
| drop | approved retention | archive proof + audit | ch28 |
| query cold data | archive contract | result/SLO test | ch16 |
生命周期控制表
可在 control plane 维护:
但不要让这张表成为未经核验的自动 drop 开关。状态转换必须读取数据库 catalog、归档 系统和审批事实;control row 只是审计协调,不是单独真相。
本节检查清单
延伸阅读
- PostgreSQL 18:Table Partitioning
- PostgreSQL 18:ALTER TABLE ATTACH/DETACH
- PostgreSQL 18:Updating Planner Statistics
- PostgreSQL 18:
pg_partition_tree
上一节:膨胀与重建 · 返回本章目录 · 下一节:amcheck 与例行完整性检查 ·
查看全书目录 · 查看索引中心
28.6 `amcheck` 与例行完整性检查
备份没有报错,不表示每个 B-tree 都满足搜索不变量;checksum 开启,也不表示 heap 与 index 逻辑一致。
完整性不是一个布尔值,而是多层故障面:
amcheck 覆盖其中重要的一段:heap 与若干 index access method 的结构/逻辑检查。它是
检测工具,不是修复器,更不是 backup 或 restore 的替代品。
28.6.1 bt_index_check 与更深检查的成本
bt_index_check:日常 B-tree 基线
它检查多种 B-tree 不变量。若发现逻辑不一致,通常抛错;无输出/无错意味着:
在本次检查覆盖的范围内没有发现问题。
不意味着:
这个索引、heap、存储设备和所有备份绝对无损坏。
锁:
与普通 SELECT 使用的 relation lock mode 相同。官方文档把它视为 live production
日常轻量检查的较好折中。
先筛合法对象
批量检查不要把 temp、invalid、非 B-tree 对象误传:
对 unique index,checkunique=true 检查重复 entry 中不应有多个可见版本;这是额外
工作,但更贴合 uniqueness 语义。
bt_index_parent_check:更强锁、更深结构
它是 bt_index_check 的超集:
- 检查 parent/child relationship;
- 检查缺失 downlink;
rootdescend=true为每个 leaf tuple 从 root 重新搜索;- 可结合 heapallindexed/checkunique。
代价:
这会阻止 concurrent INSERT/UPDATE/DELETE,也阻止 relation 的 VACUUM 和其他
utility command。锁只在函数运行期间持有,不是整个外部 transaction 都持有;但大型
索引检查时间可能很长,所以仍需要窗口。
它不能在 hot standby 上执行;bt_index_check 可以。不要在 replica 迁移脚本里把两者
当同义函数。
rootdescend 不一定是最有价值的生产检查
rootdescend 最初也服务于 B-tree feature development。它可能显著增加资源和时间,
但对现实中某些损坏类型的额外检出价值有限。
分层策略:
不要因为参数叫“更彻底”就每天全库打开。
不止 B-tree
PostgreSQL 18 amcheck 还提供:
verify_heapam 检查 table/sequence/materialized view 的物理格式和逻辑结构,返回每个
发现问题的 block/offset/attribute/message。
边界:
check_toast=true很慢;- TOAST 或其 index 损坏时,检查 toast value 理论上可能导致 server crash,很多情况 会只报错,但不能当无风险;
skip=all-visible/all-frozen降低成本,也降低覆盖;startblock/endblock可做 chunk;- 依赖的内部设施本身损坏时,函数可能无法继续。
对疑似 heap corruption,先按事故窗口、备份和证据保全运行,不要把全表
verify_heapam(check_toast=true) 当 cron。
调试日志
交互诊断可:
会显示更多检查上下文。生产自动化默认不应把 DEBUG 细节写入公开日志;错误信息可能 泄漏数据结构或可推断内容。
权限不是“能执行即可”
amcheck function 可授权给非超级用户,但官方文档提示安全与隐私风险。独立维护角色应:
Pigsty managed cluster 可用专门运维角色和私有日志采集;不要让普通应用角色在任意表上 调用结构取证函数。
28.6.2 heapallindexed、锁与业务窗口
heapallindexed 回答更强的问题
普通 B-tree structure check 主要从 index 看 index。heapallindexed=true 增加:
每一个应该有 index entry 的 heap tuple,是否都能在目标 index 的摘要结构中找到?
内部类似一次“dummy CREATE INDEX CONCURRENTLY”:
这能发现 heap/index 不一致,而这种 cross-check 不会在普通 index scan 中自动完成。
它是概率摘要
摘要受 maintenance_work_mem 限制。PostgreSQL 官方说明,为使每个应被索引的 heap
tuple 漏检不一致的概率不超过约 2%,近似需要每 tuple 2 bytes memory;更少内存时,
漏检概率缓慢上升。
因此:
仍不是数学上的 100% 证明。例行重复检查会给单个缺失/畸形 tuple 新的发现机会。
计划 memory:
只是质量量级,不是固定 allocation。对于 1 billion tuples,2 GB 量级已超过许多默认
maintenance_work_mem;不能以为 boolean 参数零成本。
heapallindexed 不改变 relation lock mode
对同一个函数:
但运行时间和 I/O 通常增加数倍,锁持有时长随之增加。锁 mode 没变,不等于业务 影响没变。
业务窗口要看四个预算
若使用 parent check,再明确:
pg_amcheck 批量编排
PostgreSQL client utility:
更深:
注意:
--parent-check/--rootdescend会用更强 relation locks;--rootdescend隐式选择 parent check;--jobs是并发连接,直接放大 server I/O/CPU/lock impact;- 默认选 table 时也检查 dependent B-tree indexes 和 TOAST;
--install-missing会改变数据库,应纳入 extension change,而不是巡检时隐式执行;- PostgreSQL 18 的
pg_amcheck文档说明该工具面向 PostgreSQL 14+ server; - client/server 版本和 option 集必须在 automation 中记录。
不要一次扫全库最大并发
较稳妥:
对象优先级:
同一时段避免叠加:
结果模型
每个对象至少记录:
“cron exit 0”没有对象级 coverage,无法证明哪些 relation 被跳过。
正式实验
一次性 churn_pkey:
表仅 50,000 live rows、无 concurrent business writes,时间只用于证明流程,不能作为 生产吞吐基准。实验在检查后还执行 concurrent reindex,并验证:
完整性检查与重建后 catalog 状态形成闭环。
28.6.3 amcheck 不替代数据页 checksum 和备份恢复
四种证明覆盖不同问题
| 机制 | 主要回答 | 不能回答 |
|---|---|---|
| data checksum | 读到的数据页 bytes 是否匹配上次写入 checksum | B-tree 排序/heap-index 逻辑、可恢复性 |
amcheck |
relation/access-method 结构与部分逻辑对应是否一致 | 所有磁盘 bytes、所有业务语义、备份可用 |
| backup manifest verification | 备份文件/size/checksum/所需 WAL 是否匹配 manifest | server 一定能恢复、业务结果正确 |
| restore drill | 能否启动、replay、打开并验证数据/SLO | 所有未来故障都可恢复 |
这四层是相加,不是互相替代。
data checksum 的边界
PostgreSQL 18 默认启用 page checksum,但可被 cluster-level 禁用。确认:
启用时:
- data page 写入时更新 checksum;
- 每次读取 page 时验证;
- 只保护 data pages;
- 不覆盖内部数据结构和 temporary files;
- cluster 级启停,不是 per-table。
page 已在 shared buffer 时,amcheck 可能检查的是 buffer 中版本,不一定在该时刻重新读 filesystem;它若触发磁盘读且 checksum 失败,也可能报 checksum error。
第 28 章 run 记录:
但这仍不意味着 storage、RAM 或所有 page 被本次实验读过。
amcheck 能发现 checksum 看不到的东西
例如:
checksum 只知道 bytes 是否与写入时一致;错误逻辑也可以被“正确”地写入并拥有有效 checksum。
备份验证也不是恢复
pg_verifybackup 可:
- 读 backup manifest;
- 检查 system identifier/manifest checksum;
- 对比缺失、额外、size 不同文件;
- 比较 file checksum;
- 对 plain backup 解析恢复所需 WAL。
官方文档同样明确:
即使 verify 通过,也应做 test restore,并验证数据库可运行、数据正确。
原因:
Pigsty 的 pgBackRest backup/PITR 流程应同时产出 repository check、restore drill 和业务 验收;第 31 章会完整展开恢复证明。
amcheck 通过不等于“备份健康”
可能:
也可能:
还可能:
需要按 failure domain 分别检查。
发现 corruption 后不要立即“修”
第一反应不应是:
先:
如果确认只有可重建 secondary index 损坏,reindex 可能合理;如果 heap、TOAST、system catalog 或 multiple copies 损坏,简单 reindex 可能失败或掩盖证据。
转入:
不同结果的安全路由
| 结果 | 动作 |
|---|---|
| check pass | 记录 coverage/time/version;继续其他层 |
| lock timeout | 本轮 inconclusive;换窗口,不算 pass |
| query canceled/resource gate | inconclusive;缩 scope/jobs |
| checksum failure | corruption incident;保全证据 |
| amcheck invariant error | corruption incident;定位 heap/index |
| server crash during deep check | highest severity;停止重复触发 |
| invalid index only | 依赖/heap 验证后评估 concurrent reindex |
| backup verify pass | 仍做 restore drill |
例行计划
频率应按数据变化量和 failure risk,不按“每月一号”机械套用。
完整性 SLO
这样“例行检查”才是可审计服务,不是散落脚本。
本节检查清单
延伸阅读
- PostgreSQL 18:amcheck
- PostgreSQL 18:pg_amcheck
- PostgreSQL 18:Data Checksums
- PostgreSQL 18:pg_verifybackup
- PostgreSQL 18:Backup Manifest Format
上一节:分区生命周期 · 返回本章目录 · 下一节:实战:建立维护节奏 · 查看全书目录 · 查看索引中心
28.7 实战:建立维护节奏
前六节分别讨论了旧版本、autovacuum、冻结、膨胀、分区生命周期和物理完整性。
这些知识如果只停在若干 SQL 和参数上,仍然很容易变成“告警来了就跑一次
VACUUM”的被动运维。本节把它们收束成一条可重复的维护闭环:
本节同时提供一套可复现实验。它不是把固定阈值塞给读者,而是让读者亲眼验证四件事:
- 旧快照怎样改变普通
VACUUM的清理结果; - “空间已经可复用”和“文件已经缩小”为什么是两个结论;
- 索引检查、并发重建与分区退役怎样分别验收;
- 哪些异常仍属于日常维护,哪些必须立即转入资源事故或数据救援。
28.7.1 制造膨胀、长事务与分区到期
先读实验合同
本章实验只允许在已经确认的 Pigsty pg-test 开发沙箱执行,正式参考环境为:
完整合同在
static/labs/ch28/lab-contract.md,机器可校验的要求与动作白名单
分别在
requirements.json 和
maintenance-contract.json。
实验只创建带随机 run_id 注释的一次性数据库和角色。为避免 exporter 在建库与授权之间
抢先连接,runner 依次执行:
所有表都位于夹具数据库。只有 maint.churn 的表级 autovacuum 被临时关闭,以便让手工
VACUUM 的因果关系可复现;这不是生产建议。实验明确禁止:
换言之,这里制造的是可丢弃夹具上的现象,不是在真实库中“先破坏再学习”。
先锁定输入与上游证据
capture 在任何写入前验证:
- 第 19 章部署证据仍指向同一个非生产沙箱;
- 第 25 章确认目标为主库,并保留观测基线;
- 第 27 章没有把试验参数持久化;
- 数据库与角色在起点均不存在;
amcheck、pg_freespacemap、pg_visibility、pgstattuple可安装;- PostgreSQL 设置、复制槽、prepared transaction、文件系统空间和校验和状态可读;
- 11 个实验源文件的 SHA-256 与随后执行的版本一致。
这一步解决一个经常被忽略的问题:如果运行期间脚本、目标或前置状态发生变化,最终数字 即使“看起来正确”,也不能归到当前实验设计上。runner 因此在 capture 之后再次计算源文件 散列,不一致就失败关闭。
创建三组现象
夹具包含两类表。
第一类是一个 fillfactor = 70 的 heap,共写入 60,000 行,并建立主键和
(status, id) B-tree:
第二类是按日期范围分区的事件表:
数据内容、行数和边界都是确定的,因此归档前后可以比较:
仅比较文件大小或只执行一次 count(*) 都不够:前者不能证明逻辑内容,后者无法发现
同样行数下的值篡改。
建立一个可识别的旧快照
实验另开一个连接:
连接的 application_name 固定为 pg36-ch28-old-snapshot。runner 必须从
pg_stat_activity 同时看到:
只有这五个条件全匹配,后续才允许释放该会话。脚本不使用模糊的 query 文本、不按用户名
批量杀连接,也不把所有 idle in transaction 一锅端。
接下来在另一个事务中制造 churn:
结果应为:
remaining_rows 必须在下一条 SQL 命令中读取。PostgreSQL 的 data-modifying CTE
共享同一个命令级快照;若在同一条语句里再次 count(*),读到的仍可能是修改前的
60,000 行。这不是数据库“少提交了一次”,而是命令快照语义。类似地,不能让
UPDATE 与 DELETE 命中同一批行,再假设两个子语句会按书写顺序串行处理。
为什么这组夹具有教学价值
这组实验同时保留了四条互不替代的证据线:
| 现象 | 主要证据 | 回答的问题 |
|---|---|---|
| 旧快照 | backend_xmin、精确 holder identity |
谁还需要旧版本 |
| heap churn | pgstattuple、FSM、VM、关系大小 |
旧版本是否清掉、空间去哪里 |
| 索引维护 | amcheck、catalog、relfilenode |
结构是否通过检查、重建是否完成 |
| 分区到期 | 分区拓扑、CSV、manifest、回灌 | 数据是否先可恢复、再退出热表 |
任何一列都不能替代其他列。n_dead_tup 是估计值;文件大小不是可见性;amcheck
不是备份;CSV 存在也不等于可恢复。
28.7.2 从指标与原生视图判定维护优先级
先按风险排序,不按表大小排序
维护队列应该先回答“拖延会造成什么”,而不是“哪个数字最大”。一个实用的三层优先级是:
因此,一个 relfrozenxid 年龄危险但只有 2 GB 的表,可能比一个 2 TB、膨胀 20%、
仍有充足空间且正常被 vacuum 的表更紧急。维护分数可以帮助排序,但不能把 P0 平均进
一个漂亮的加权总分:
同层内再用 headroom、增长速率、业务关键度、预计锁时间和维护成本排序。
第一屏:全库安全边界
先看数据库年龄、活动快照、prepared transaction 和复制槽:
不要把“最老连接”自动等同于“清理阻塞者”。真正相关的是它是否持有旧
backend_xmin、prepared XID 或 slot xmin/catalog_xmin,以及时间线是否与问题吻合。
也不要看到 age() 大就立即运行一条万能命令;先按 28.3 的版本化流程判断处于常规态、
迫近 failsafe,还是已经进入事务 ID 硬停机状态。
第二屏:对象触发与进度
候选表至少需要这些原生信息:
其中:
pg_stat_user_tables是累计统计和估计,适合发现趋势,不是物理真值;pg_class给出 catalog 估计、年龄和大小,仍不能直接证明 dead tuple 百分比;pg_stat_progress_vacuum只能描述正在运行的进度,phase 切换不是线性 ETA;- 需要高成本物理确认时,再在已选对象上使用
pgstattuple,不要全库高频扫描; - 用
pg_visibility_map_summary和pg_freespace分别观察 VM 与 FSM,但不要把它们 解释成业务行正确性。
例如对单个已获批候选:
将 Pigsty 看板与 SQL 对齐
Pigsty 的 PostgreSQL 监控提供数据库、实例、表、查询、复制、WAL、磁盘和 autovacuum 等多层视图。看板负责快速发现关联:
原生视图负责复核对象、持有者、年龄、命令 phase 和 catalog 状态。正确的工作方式是:
不要从一张图直接跳到 VACUUM FULL、REINDEX 或终止会话。图表的采样、标签聚合和
保留周期都可能掩盖瞬态事实;反过来,单次 SQL 也无法替代时间序列。
本次实验如何判定
正式 run 的基线与 churn 后快照同时记录:
优先级判断如下:
- 没有 XID/MXID 或完整性 P0 信号;
- 人工旧快照明确保留 50,000 个物理 dead tuples,先解除精确保留者;
- 普通 vacuum 之后确认空间回收语义,再决定是否需要文件重写;
- 索引检查、并发重建和分区到期作为独立、可验收的维护动作执行。
这里的重要判断不是“dead tuple 多,所以 vacuum”,而是“先证明谁让 vacuum 不能完成, 再移除那个精确原因”。
28.7.3 执行清理、检查和分区退役并验证副作用
运行完整闭环
先做不接触数据库的合同检查:
然后为每次正式实验使用一个不存在或为空的私密绝对目录:
all 的顺序固定为:
也可以逐步执行:
capture 与 all 拒绝覆盖非空证据目录。私有目录包含原始 SQL 输出、进度采样、
归档 CSV、清理记录和 source hashes;公开仓库只保存字段白名单后的
maintenance-run.json,不保存口令、连接串、SSH
材料或原始业务数据。
第一步:旧快照存在时只运行普通 VACUUM
在 holder 仍有非空 backend_xmin 时,runner 对夹具表运行普通 VACUUM,并以较低的
session-local cost 设置放慢它,以便旁路采样:
正式 run 结果:
| 项目 | 结果 |
|---|---|
| 初始行数 | 60,000 |
| 更新行数 | 40,000 |
| 删除行数 | 10,000 |
| 当前行数 | 50,000 |
观察到 holder backend_xmin |
是 |
| vacuum 后物理 dead tuples | 50,000 |
| 进度采样 | 128 |
| 观察到 phase | initializing、scanning heap |
这不是“VACUUM 失效”。它遵守 MVCC,不能移除那个旧快照仍可能读取的版本。若此时反复
提高 cost limit、增加 worker 或改用 VACUUM FULL,都没有解决保留边界,反而会把
问题扩大成资源或锁事故。
第二步:只释放精确 holder,再完成冻结与统计
runner 只允许终止同时匹配数据库、角色、application、记录 PID 和非空
backend_xmin 的那一个夹具会话。任一属性改变都拒绝动作。释放后执行:
这里的 FREEZE 是为了在可控夹具上展示冻结与 VM 变化,不是声称所有日常 vacuum 都
必须加 FREEZE,也不是 PostgreSQL 18 已进入 XID 硬停机后的万能命令。
正式结果:
| 证据 | 基线 / 旧快照阶段 | 释放后 |
|---|---|---|
| 物理 dead tuples | 50,000(旧快照下 vacuum 后) | 0 |
| heap bytes | 61,440,000(初始) | 87,040,000 |
| FSM 可用字节 | — | 51,920,000 |
| all-visible pages | — | 10,625 |
| all-frozen pages | — | 10,625 |
age(relfrozenxid) |
14 | 2 |
| 进度采样 | 128 | 171 |
| 释放后 phase | — | scan heap、vacuum indexes、vacuum heap |
结论需要精确措辞:
初始 heap 为 61,440,000 字节,churn 后最终仍为 87,040,000 字节。普通 VACUUM
成功清理,并不承诺把中间空洞归还操作系统。它在适当条件下可能截断文件末尾的空页,
但这次证据不能推导出“普通 vacuum 永不缩文件”,也不能推导出“文件没缩,所以 vacuum
没用”。
第三步:检查索引,再验证并发重建
runner 先对主键执行两层 amcheck:
正式 run:
第二种检查更强,但会取得 ShareLock,阻塞并发 DML 与 VACUUM;不能因为本次只需
0.08 秒就假设大表也能随时运行。生产中应根据表大小、缓存状态、业务窗口和副本角色
安排,必要时先用较轻的 bt_index_check 或 pg_amcheck 分批巡检。
然后对次级索引执行:
验收不止是“命令返回 0”:
| 条件 | 正式结果 |
|---|---|
relfilenode 改变 |
是 |
| 原大小 | 3,227,648 bytes |
| 新大小 | 1,589,248 bytes |
| 同名有效索引 | 1 |
indisvalid / indisready / indislive |
全部为真 |
| 无效夹具索引 | 0 |
_ccnew / _ccold 残留 |
0 |
尺寸变小是这次数据分布的结果,不是并发重建的固定收益。真正的最低验收是新索引有效、 唯一目标明确、没有异常残留,并且写放大、临时空间、WAL、复制延迟和锁等待仍在预算内。
第四步:先分离、归档与回灌,再删除
过期分区执行:
DETACH ... CONCURRENTLY 不能放在显式事务块中;它分两次事务完成,存在 default
partition 时也不能直接使用。若中途留下 pending detach,应该检查 catalog 与作业状态,
按实际版本使用 FINALIZE,而不是盲目再次执行或直接 drop。
正式 run 的顺序和结果:
SHA-256 完整值在公开结果文件中为:
独立回灌表的行数、日期范围、金额合计和有序 digest 必须与 detach 前 manifest 一致。
只有 round_trip_validated = true 后,runner 才允许删除原分区。这条门槛把“对象已经
离开热路径”和“数据已经可以销毁”明确分开。
第五步:证明副作用已经收束
正式 run 最终证明:
验证器拒绝 28 个预声明反例,并对 14 个现场证据 mutant 验证失败关闭。review 校验私有
证据文件、归档、源散列和公开摘要。这样的负向验证很重要:只证明“正确文件能通过”
无法证明校验器真的会拦住数据库未清理、索引无效、digest 不一致或误用
VACUUM FULL 等失败状态。
一次沙箱成功只说明合同内流程可执行。它没有证明:
- 同样动作在生产表上的锁时间和 I/O 成本;
- 当前备份真的可恢复;
amcheck能替代 checksums、pg_verifybackup或恢复演练;- 生产窗口已经获批;
- 任何固定 dead tuple 比例适合所有表。
28.7.4 输出维护清单及 ch34/ch35 的安全路由
把“维护”拆成不同频率
健康维护不是每月一次“大扫除”,而是多种频率的闭环。
| 频率 | 观察与动作 | 必须留下的证据 |
|---|---|---|
| 连续 | XID/MXID headroom、磁盘、autovacuum backlog、复制槽、长事务、错误日志 | 告警身份、开始时间、当前 headroom |
| 每日 | top dead/churn 表、长事务、slot/prepared xact、失败 vacuum、分区边界 | 排序快照、owner、处置状态 |
| 每周 | autovacuum 覆盖、统计陈旧、HOT 比例、FSM/VM 抽查、索引增长 | 趋势、候选对象、是否需要实验 |
| 每月 | 表级 override 审计、冻结策略、空间与 WAL 预算、归档回灌抽检 | 参数来源、容量预算、恢复证据 |
| 每季度 | checksums/pg_amcheck 策略、备份 manifest、整库恢复演练 |
完整性报告、恢复 RTO/RPO |
| 事件驱动 | 大批量导入/更新/删除、版本升级、分区切换、schema change 后 | 前后统计、对象状态、回退结果 |
频率不是硬编码。写入速率、表大小、SLO、冻结 headroom 和恢复要求决定实际周期。 关键是每项都有 owner、阈值来源、验收和升级路线。
一张可执行的生产工单
任何主动维护动作至少填写:
没有 target OID、精确命令、预算和停手条件的工单,不应进入生产。
Pigsty 命令是入口,不是审批
在目标节点上,Pigsty 提供本地 PostgreSQL 维护封装:
它们分别封装 vacuumdb 或 pg_repack,降低日常操作摩擦,但不改变 PostgreSQL
本身的锁、WAL、磁盘、MVCC 和版本语义。特别注意:
- 不要把
pig pg vacuum mydb --full当作常规清理;VACUUM FULL需要ACCESS EXCLUSIVE并重写表; pig pg freeze适用于明确的冻结任务,不是 PostgreSQL 18 事务 ID 硬停机状态下 可以无条件执行的急救口诀;pig pg repack --plan先列出计划对象,但正式执行仍要预算额外空间、写放大、 WAL、复制延迟和最终锁;pig pg kill默认 dry-run 是好习惯;即使加-x,也必须先用 PID、数据库、角色、 application、事务时间和保留证据锁定目标。
平台让命令一致,证据链才决定命令是否应该执行。
明确停手条件
以下任何条件出现,都应停止扩大动作,保留现场并重新判断:
“命令还在跑”不是继续等待的充分理由;“已经跑了很久”也不是取消的充分理由。应根据 预先声明的预算、progress phase、阻塞图和副作用趋势决定。
分流到第 34 章:资源与过载事故
如果数据结构没有明确损坏,但维护动作或 backlog 正在威胁服务,应转入 第 34 章:过载保护与资源故障判型:
第 34 章回答的是“如何止血、保护前台、恢复资源 headroom,并在容量与并发边界内重排 维护”,不是在压力中继续加大 vacuum 或 reindex 并发。
分流到第 35 章:数据救援与取证
出现以下证据时,应停止把问题称为“普通膨胀”,转入 第 35 章:数据抢救与工程取证:
进入救援路线后,优先保护证据和可恢复性:
不要在唯一生产副本上反复尝试 zero_damaged_pages、手工删文件或未经验证的 catalog
修改。这些动作可能把可调查的局部损坏变成不可逆的数据丢失。
本章最终验收清单
完成第 28 章后,读者应该能逐项回答:
- 我能用触发公式、表级 override 与版本参数解释 autovacuum 为什么启动;
- 我能区分统计估计、物理抽查、VM、FSM 和关系大小各自证明什么;
- 我能找到实际保留旧版本的事务、prepared xact 或 replication slot;
- 我能同时检查 XID 与 MXID headroom,而不是只背一个 wraparound 数字;
- 我知道普通
VACUUM、VACUUM FULL、REINDEX CONCURRENTLY与pg_repack的锁、空间和 WAL 边界; - 我能把分区 detach、归档、回灌验证和最终删除拆成独立门槛;
- 我能为
amcheck选择检查层级与窗口,并知道它不能替代备份恢复; - 我能写出含目标、假设、预算、停手、验收和恢复的生产维护工单;
- 我能判断事件应留在日常维护,还是升级到第 34 或第 35 章;
- 我不会把一次沙箱成功描述成生产安全证明。
本章参考实验的最终判定为:
这三个结论缺一不可:流程已经跑通,副作用已经收束,但生产变更仍未获授权。
进一步阅读:
- PostgreSQL 18:Routine Vacuuming
- PostgreSQL 18:VACUUM
- PostgreSQL 18:VACUUM Progress Reporting
- PostgreSQL 18:Table Partitioning
- PostgreSQL 18:REINDEX
- PostgreSQL 18:amcheck
- Pigsty:PostgreSQL Monitoring
- Pigsty:
pig pgMaintenance Commands
上一节:amcheck 与例行完整性检查 · 返回本章目录 · 下一章:移花接木:逻辑复制、迁移与异构同步 ·
查看全书目录 · 查看索引中心