# `amcheck` 与例行完整性检查

LLMS 索引： [llms.txt](/llms.txt)

---

备份没有报错，不表示每个 B-tree 都满足搜索不变量；checksum 开启，也不表示 heap 与
index 逻辑一致。

完整性不是一个布尔值，而是多层故障面：

```text
page bytes
  -> relation structure
      -> heap/index logical correspondence
          -> constraints and business invariants
              -> backup artifacts
                  -> actual recoverability
```

`amcheck` 覆盖其中重要的一段：heap 与若干 index access method 的结构/逻辑检查。它是
检测工具，不是修复器，更不是 backup 或 restore 的替代品。

## 28.6.1 `bt_index_check` 与更深检查的成本 {#item-28-6-1}

### `bt_index_check`：日常 B-tree 基线

```sql
CREATE EXTENSION amcheck;

SELECT bt_index_check(
  index => 'app.orders_pkey'::regclass,
  heapallindexed => false,
  checkunique => true
);
```

它检查多种 B-tree 不变量。若发现逻辑不一致，通常抛错；无输出/无错意味着：

> 在本次检查覆盖的范围内没有发现问题。

不意味着：

> 这个索引、heap、存储设备和所有备份绝对无损坏。

锁：

```text
index  AccessShareLock
heap   AccessShareLock
```

与普通 `SELECT` 使用的 relation lock mode 相同。官方文档把它视为 live production
日常轻量检查的较好折中。

### 先筛合法对象

批量检查不要把 temp、invalid、非 B-tree 对象误传：

```sql
SELECT
  bt_index_check(
    index => c.oid,
    heapallindexed => i.indisunique,
    checkunique => i.indisunique
  ),
  n.nspname,
  c.relname,
  c.relpages
FROM pg_index AS i
JOIN pg_class AS c ON c.oid = i.indexrelid
JOIN pg_namespace AS n ON n.oid = c.relnamespace
JOIN pg_am AS am ON am.oid = c.relam
WHERE am.amname = 'btree'
  AND c.relkind = 'i'
  AND c.relpersistence <> 't'
  AND i.indisready
  AND i.indisvalid
ORDER BY c.relpages DESC;
```

对 unique index，`checkunique=true` 检查重复 entry 中不应有多个可见版本；这是额外
工作，但更贴合 uniqueness 语义。

### `bt_index_parent_check`：更强锁、更深结构

```sql
SELECT bt_index_parent_check(
  index => 'app.orders_pkey'::regclass,
  heapallindexed => true,
  rootdescend => true,
  checkunique => true
);
```

它是 `bt_index_check` 的超集：

- 检查 parent/child relationship；
- 检查缺失 downlink；
- `rootdescend=true` 为每个 leaf tuple 从 root 重新搜索；
- 可结合 heapallindexed/checkunique。

代价：

```text
index  ShareLock
heap   ShareLock
```

这会阻止 concurrent `INSERT/UPDATE/DELETE`，也阻止 relation 的 `VACUUM` 和其他
utility command。锁只在函数运行期间持有，不是整个外部 transaction 都持有；但大型
索引检查时间可能很长，所以仍需要窗口。

它不能在 hot standby 上执行；`bt_index_check` 可以。不要在 replica 迁移脚本里把两者
当同义函数。

### `rootdescend` 不一定是最有价值的生产检查

`rootdescend` 最初也服务于 B-tree feature development。它可能显著增加资源和时间，
但对现实中某些损坏类型的额外检出价值有限。

分层策略：

```text
routine:
  bt_index_check

selected stronger check:
  bt_index_check + heapallindexed + checkunique

maintenance window / incident:
  bt_index_parent_check
  optional rootdescend
```

不要因为参数叫“更彻底”就每天全库打开。

### 不止 B-tree

PostgreSQL 18 `amcheck` 还提供：

```text
gin_index_check
verify_heapam
```

`verify_heapam` 检查 table/sequence/materialized view 的物理格式和逻辑结构，返回每个
发现问题的 block/offset/attribute/message。

```sql
SELECT *
FROM verify_heapam(
  relation => 'app.orders'::regclass,
  on_error_stop => false,
  check_toast => false,
  skip => 'none',
  startblock => NULL,
  endblock => NULL
);
```

边界：

- `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。

### 调试日志

交互诊断可：

```sql
SET client_min_messages = DEBUG1;
SELECT bt_index_check('app.orders_pkey', true, true);
```

会显示更多检查上下文。生产自动化默认不应把 DEBUG 细节写入公开日志；错误信息可能
泄漏数据结构或可推断内容。

### 权限不是“能执行即可”

amcheck function 可授权给非超级用户，但官方文档提示安全与隐私风险。独立维护角色应：

```text
no application writes
no broad role membership
function execute only where needed
secure log/evidence destination
audited schedule
no public error payload
```

Pigsty managed cluster 可用专门运维角色和私有日志采集；不要让普通应用角色在任意表上
调用结构取证函数。

## 28.6.2 `heapallindexed`、锁与业务窗口 {#item-28-6-2}

### `heapallindexed` 回答更强的问题

普通 B-tree structure check 主要从 index 看 index。`heapallindexed=true` 增加：

> 每一个应该有 index entry 的 heap tuple，是否都能在目标 index 的摘要结构中找到？

内部类似一次“dummy `CREATE INDEX CONCURRENTLY`”：

```text
scan target index
  -> build in-memory fingerprint summary
      -> scan heap as hypothetical index input
          -> verify expected entries are represented
```

这能发现 heap/index 不一致，而这种 cross-check 不会在普通 index scan 中自动完成。

### 它是概率摘要

摘要受 `maintenance_work_mem` 限制。PostgreSQL 官方说明，为使每个应被索引的 heap
tuple 漏检不一致的概率不超过约 2%，近似需要每 tuple 2 bytes memory；更少内存时，
漏检概率缓慢上升。

因此：

```text
heapallindexed passed once
```

仍不是数学上的 100% 证明。例行重复检查会给单个缺失/畸形 tuple 新的发现机会。

计划 memory：

$$
M_{\text{fingerprint}} \approx 2 \times N_{\text{tuples}}
$$

只是质量量级，不是固定 allocation。对于 1 billion tuples，2 GB 量级已超过许多默认
`maintenance_work_mem`；不能以为 boolean 参数零成本。

### `heapallindexed` 不改变 relation lock mode

对同一个函数：

```text
bt_index_check(heapallindexed=false/true)
  -> AccessShareLock remains

bt_index_parent_check(heapallindexed=false/true)
  -> ShareLock remains
```

但运行时间和 I/O 通常增加数倍，锁**持有时长**随之增加。锁 mode 没变，不等于业务
影响没变。

### 业务窗口要看四个预算

```yaml
scope:
  relations: [...]
  total_bytes: ...
  total_tuples: ...

locks:
  function: bt_index_check
  mode: AccessShareLock
  lock_timeout: ...

resources:
  maintenance_work_mem: ...
  jobs: ...
  io_budget: ...
  cpu_budget: ...

service:
  p95_budget: ...
  replica_lag_budget: ...
  abort_threshold: ...
```

若使用 parent check，再明确：

```text
write blocking expected
maximum check duration
application retry behavior
DDL/vacuum exclusion
```

### `pg_amcheck` 批量编排

PostgreSQL client utility：

```bash
pg_amcheck \
  --database=appdb \
  --schema=app \
  --heapallindexed \
  --checkunique \
  --progress \
  --jobs=2
```

更深：

```bash
pg_amcheck \
  --database=appdb \
  --table=app.critical_orders \
  --parent-check \
  --heapallindexed \
  --checkunique \
  --jobs=1
```

注意：

- `--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 中记录。

### 不要一次扫全库最大并发

较稳妥：

```text
catalog + small critical indexes
  -> largest/highest-risk index batches
      -> rotating coverage
          -> periodic deep window
```

对象优先级：

```text
constraint/primary indexes
high write volume
recent crash/storage incident
collation version change
replica divergence suspicion
large/old/rarely read objects
previous invalid/reindex artifact
```

同一时段避免叠加：

```text
backup full scan
vacuum freeze
reindex
scrub
bulk load
major analytical scan
```

### 结果模型

每个对象至少记录：

```json
{
  "database": "appdb",
  "relation": "app.orders_pkey",
  "function": "bt_index_check",
  "heapallindexed": true,
  "checkunique": true,
  "parent_check": false,
  "rootdescend": false,
  "started_at": "...",
  "ended_at": "...",
  "server_version": "18.x",
  "maintenance_work_mem": "...",
  "result": "passed",
  "error_sqlstate": null
}
```

“cron exit 0”没有对象级 coverage，无法证明哪些 relation 被跳过。

### 正式实验

一次性 `churn_pkey`：

```text
bt_index_check(
  heapallindexed=true,
  checkunique=true
)                                      pass, 0.0597 s

bt_index_parent_check(
  heapallindexed=true,
  rootdescend=true,
  checkunique=true
)                                      pass, 0.0789 s
```

表仅 50,000 live rows、无 concurrent business writes，时间只用于证明流程，不能作为
生产吞吐基准。实验在检查后还执行 concurrent reindex，并验证：

```text
one final named index
indisready=true
indisvalid=true
indislive=true
invalid fixture indexes=0
cc artifacts=0
```

完整性检查与重建后 catalog 状态形成闭环。

## 28.6.3 `amcheck` 不替代数据页 checksum 和备份恢复 {#item-28-6-3}

### 四种证明覆盖不同问题

| 机制 | 主要回答 | 不能回答 |
|---|---|---|
| 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 禁用。确认：

```sql
SHOW data_checksums;
```

启用时：

- data page 写入时更新 checksum；
- 每次读取 page 时验证；
- 只保护 data pages；
- 不覆盖内部数据结构和 temporary files；
- cluster 级启停，不是 per-table。

page 已在 shared buffer 时，amcheck 可能检查的是 buffer 中版本，不一定在该时刻重新读
filesystem；它若触发磁盘读且 checksum 失败，也可能报 checksum error。

第 28 章 run 记录：

```text
data_checksums = on
```

但这仍不意味着 storage、RAM 或所有 page 被本次实验读过。

### `amcheck` 能发现 checksum 看不到的东西

例如：

```text
operator class violates ordering rules
OS collation behavior changes
primary/standby collation environment differs
heap tuple missing corresponding index entry
access method implementation bug
logical structure valid bytes but wrong links/order
```

checksum 只知道 bytes 是否与写入时一致；错误逻辑也可以被“正确”地写入并拥有有效
checksum。

### 备份验证也不是恢复

`pg_verifybackup` 可：

- 读 backup manifest；
- 检查 system identifier/manifest checksum；
- 对比缺失、额外、size 不同文件；
- 比较 file checksum；
- 对 plain backup 解析恢复所需 WAL。

官方文档同样明确：

> 即使 verify 通过，也应做 test restore，并验证数据库可运行、数据正确。

原因：

```text
manifest cannot prove every server recovery check
valid WAL checksum can still encode nonsensical action due to bug
archive access/KMS/network may fail
recovery config/timeline/target may be wrong
application schema/semantic checks may fail
RTO may exceed objective
```

Pigsty 的 pgBackRest backup/PITR 流程应同时产出 repository check、restore drill 和业务
验收；第 31 章会完整展开恢复证明。

### `amcheck` 通过不等于“备份健康”

可能：

```text
live primary amcheck pass
backup missing WAL
restore impossible
```

也可能：

```text
backup files verify pass
live index logically inconsistent
```

还可能：

```text
primary index inconsistent
standby has different corruption state
```

需要按 failure domain 分别检查。

### 发现 corruption 后不要立即“修”

第一反应不应是：

```text
REINDEX everything
ignore_checksum_failure=on
zero damaged page
delete relation file
fail over blindly
```

先：

```text
1. preserve error, SQLSTATE, block/index identity and logs
2. identify primary/replica/timeline/version/storage
3. stop avoidable writes to affected scope
4. check whether error reproduces on another copy
5. verify checksums, storage/kernel events and recent changes
6. validate backup and recovery points
7. classify heap vs index vs VM vs catalog vs hardware
8. choose repair/rebuild/restore with evidence
```

如果确认只有可重建 secondary index 损坏，reindex 可能合理；如果 heap、TOAST、system
catalog 或 multiple copies 损坏，简单 reindex 可能失败或掩盖证据。

转入：

[第 35 章：数据抢救与工程取证](/data-rescue-forensics/)。

### 不同结果的安全路由

| 结果 | 动作 |
|---|---|
| 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 |

### 例行计划

```text
continuous:
  checksum/log/storage alerts

daily:
  metadata inventory, invalid indexes, recent error correlation

weekly rotating:
  bt_index_check on critical/high-change B-trees

monthly/window:
  heapallindexed selected objects
  backup manifest verification

quarterly / after incident:
  parent-check/deep checks where justified
  full restore drill + business validation

after upgrade/collation/storage event:
  targeted expanded coverage
```

频率应按数据变化量和 failure risk，不按“每月一号”机械套用。

### 完整性 SLO

```yaml
coverage:
  critical_btree_days: 7
  all_btree_days: 30
  deep_selected_days: 90

backup:
  manifest_verify_each_backup: true
  restore_drill_days: 30

response:
  checksum_or_amcheck_error_page_minutes: 5
  preserve_evidence: true
  automatic_repair: false
```

这样“例行检查”才是可审计服务，不是散落脚本。

## 本节检查清单

```text
object type / validity / persistence
bt_index_check vs parent-check selection
heapallindexed/checkunique/rootdescend flags
relation lock mode and duration
maintenance_work_mem and jobs
I/O/CPU/service budget
object-level coverage record
private error evidence
data_checksums state
backup manifest verification
restore drill recency
corruption response route
no automatic destructive repair
```

## 延伸阅读

- [PostgreSQL 18：amcheck](https://www.postgresql.org/docs/18/amcheck.html)
- [PostgreSQL 18：pg_amcheck](https://www.postgresql.org/docs/18/app-pgamcheck.html)
- [PostgreSQL 18：Data Checksums](https://www.postgresql.org/docs/18/checksums.html)
- [PostgreSQL 18：pg_verifybackup](https://www.postgresql.org/docs/18/app-pgverifybackup.html)
- [PostgreSQL 18：Backup Manifest Format](https://www.postgresql.org/docs/18/backup-manifest-format.html)

---

[上一节：分区生命周期](../05/) · [返回本章目录](../) · [下一节：实战：建立维护节奏](../07/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
