# 先界定误操作

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

---

误操作事故最容易犯的第一个错误，是把“有人说删错了”直接翻译成一个 PITR 时间。事故
描述是业务语言，恢复目标是 WAL 历史上的精确边界，中间至少还缺对象、事务、提交、
后续写入和外部副作用五层证据。

先写事故合同，再碰恢复命令：

```text
affected truth       哪些业务事实错了
bad transaction      哪一笔或哪一组提交制造错误
first/last bad       错误开始和结束边界
continuing mutation  错误作业是否仍在运行
good after           错误之后哪些合法事实必须保留
external effects     数据库之外已经发生了什么
authority            谁能判定最终业务真相
```

若其中关键项仍是 unknown，正确动作通常是围住继续伤害、保存证据和并行恢复候选，而不是
在原集群上赌一个时间点。

## 32.1.1 错误 UPDATE、DELETE、DROP 与批处理 {#item-32-1-1}

### 先按状态变化分类

同一句“数据没了”，恢复语义可能完全不同：

| 类型 | 典型例子 | 首要边界 | 常见恢复方向 |
|---|---|---|---|
| 单笔 DML | `UPDATE` 漏写 `WHERE` | 一笔 commit 前后 | 逻辑修补或对象提取 |
| 多笔 DML | 客户端 autocommit 循环删除 | 第一笔与最后一笔坏提交 | 多候选、批量对账 |
| 事务型 DDL | `DROP TABLE`、`DROP SCHEMA` | DDL commit 与依赖范围 | 从隔离 PITR 提取对象 |
| 粗粒度操作 | `TRUNCATE`、分区 detach/drop | 对象与关联表 | 对象提取或整库恢复 |
| 后台批处理 | 调度器持续重算错误价格 | 作业开始、每次 commit、停止时刻 | 先停作业，再找完整坏窗 |
| 权限/配置 | 错误 `GRANT`、参数或路由变更 | 数据是否真的变化 | 配置回退，不一定 PITR |
| 外部副作用 | outbox 已发、支付已扣 | 数据库 commit 与外部确认 | 数据恢复 + 业务补偿 |

PostgreSQL 的许多 DDL 具有事务性，未提交时可以 `ROLLBACK`；一旦已经提交，就不能回到
原会话补发 `ROLLBACK`。PITR 的作用不是“撤销 SQL”，而是从较早基础备份重放 WAL，
在指定提交边界之前或之后构造另一份完整历史。

这一区别会直接改变调查方式：

```text
single transaction
  -> 找到 commit identity
  -> 验证 before / after 两个候选

autocommit batch
  -> 找到 first bad commit
  -> 找到 last bad commit
  -> 判断中间是否夹杂合法事务

external side effect
  -> 找数据库提交
  -> 找 message/payment idempotency identity
  -> 恢复数据后仍要外部对账
```

### “执行成功”不说明影响正确

应用需要把下面这些信息当作审计事件，而不是只记录 SQL 文本：

```text
change_id / job_id / request_id
application actor and database role
transaction id or commit-correlated identity
object and business-key range
expected rows / actual rows
old/new aggregate or manifest
started_at / committed_at in UTC
idempotency key and external event ids
```

`UPDATE 1000000` 的 row count 能提示范围异常，却不能说明哪一百万行本来应该改变；
statement log 能说明服务端收到什么文本，却可能没有 bind 参数；WAL 是物理恢复事实，
不是自动生成的业务审计表。恢复能力必须在事故前就布置可关联的应用、数据库和外部系统
身份。

### 批处理不是“一笔大事务”的同义词

考虑一个每 1,000 行提交一次的脚本：

```text
10:00:01  batch 1 commit  bad
10:00:03  user order      good
10:00:04  batch 2 commit  bad
10:00:06  refund          good
10:00:07  batch 3 commit  bad
```

恢复到 batch 1 之前会同时丢掉两笔合法写；恢复到 batch 3 之后又保留全部错误。此时没有
一个单独 PITR 点能直接得到最终正确业务状态。需要先取得历史正确对象，再按业务身份重放
或合并合法变化。

因此发现坏批次后，第一步是阻断**相同 mutation source**：

- 禁用精确的 scheduler job，而不是停掉所有 scheduler；
- 吊销或冻结产生错误的 service role，而不是随意改全局 HBA；
- 对受影响业务 key 加写围栏，而不是默认让全站停写；
- 记录围栏生效前最后一笔提交，不能把“点击停止”当成“已经停止”。

停止动作、实际停止证据和最后坏提交是三件事。

### 复制不会替你保留“正确过去”

流复制和同步复制的目标是复制已经提交的 WAL。误更新只要正常提交，副本也会正确地把
它重放。以下判断都不成立：

```text
replica lag = 0       -> 副本数据一定正确
three replicas        -> 一定存在错误之前的副本
fail over             -> 误更新会消失
backup job succeeded  -> 一定能定位到正确边界
```

若团队希望保留延迟副本，必须把延迟窗口、监控、promotion 防误触和 WAL 保留纳入独立
设计；它也不能替代基础备份、连续归档和恢复演练。

## 32.1.2 影响对象、开始时间、结束时间和持续写入 {#item-32-1-2}

### 建立事故包络

把事故范围写成一个六维包络：

$$
I=(O,K,[T_f,T_l],X,A,E)
$$

- $O$：database、schema、table、partition、sequence、large object 等对象集合；
- $K$：受影响业务 key 或谓词；
- $[T_f,T_l]$：第一笔到最后一笔坏提交的区间；
- $X$：已确认或候选 transaction identity；
- $A$：坏提交之后必须保留的合法写集合；
- $E$：消息、支付、邮件、搜索索引、缓存等外部副作用。

不要把“发现时间”写进 $T_l$。四个时刻应分开：

```text
t_start    错误动作开始
t_commit   某笔错误事务真正提交
t_detect   人或监控发现异常
t_fence    错误来源已被证明停止
```

若一个作业持续运行，则 `t_fence` 之前仍可能出现新坏提交，事故窗口是移动的。先建立写
围栏并确认它生效，才有稳定的恢复问题。

### 从多层证据收敛，而不是信一个时间戳

| 证据层 | 能回答 | 不能单独回答 |
|---|---|---|
| 工单/聊天 | 人声称何时做了什么 | 实际 commit 边界 |
| 应用审计 | request、actor、业务 key、idempotency | 数据库是否提交 |
| PostgreSQL log | session、statement、error、duration | 未记录的参数与业务正确性 |
| 审计表 | run、stage、XID、LSN、对象摘要 | 外部系统是否消费 |
| 当前表状态 | 现在受影响多少 | 历史旧值 |
| backup/WAL catalog | 可恢复血缘与覆盖范围 | 哪个业务状态正确 |
| 外部账本 | 支付、消息、邮件结果 | 数据库内部完整状态 |

`pg_stat_activity` 适合回答“现在什么还在运行”，不是历史审计；累计统计也不能还原某笔
事务。对关键批处理，最好在同一事务里写入最小 `incident_audit`：

```sql
INSERT INTO ops.change_audit (
    change_id, stage, xid, observed_lsn, details
)
VALUES (
    :'change_id',
    'before-dangerous-change',
    pg_current_xact_id(),
    pg_current_wal_lsn(),
    jsonb_build_object(
        'predicate', :'reviewed_predicate',
        'expected_rows', :expected_rows
    )
);
```

这不是让 DBA 在事故后凭空制造 XID，而是展示可恢复性如何进入变更协议。若事前没有
审计，必须用日志、应用请求、业务 key、WAL 工具和候选恢复交叉定位，并扩大不确定区间。

### 把事后合法写入作为一等对象

很多恢复票只写“删除 1,000 行”，没有写错误之后发生的 100 笔合法订单。应单独建立
`good-after manifest`：

```text
identity         order_id / ledger_id / event_id
commit evidence  audit row / immutable ledger / application event
ordering         depends-on or sequence
payload digest   canonical fields, not secret-bearing raw request
external state   not-sent / sent / acknowledged / compensated
replay method    idempotent insert / conditional update / manual review
owner            business authority
```

这个集合为空也必须有证据：例如错误后立即完成全局写围栏，且所有 writer、job 与外部
ingress 都被证明停止。“没看到新行”不等于没有合法写。

### 用上下界表达不确定性

若只能确认错误发生在 `10:00:00+08` 到 `10:00:05+08` 之间，不要假装存在一个精确
`10:00:03`。更安全的候选搜索是：

```text
candidate A  upper bound before suspected error
candidate B  first candidate where damage appears
candidate C  immediately before independently identified commit
```

每个候选都运行同一组业务探针。目标选择是一个可证伪的搜索过程，而不是一次性猜值。
恢复副本越容易创建，越应该多恢复一个候选，少在原集群上做一次不可逆猜测。

### 事故包络的完成条件

进入 restore 前至少要能填写：

```yaml
incident:
  affected_objects: [...]
  affected_business_keys: [...]
  first_bad_commit: known | bounded | unknown
  last_bad_commit: known | bounded | unknown
  mutation_source_fenced: true
  good_after_manifest: evidence-reference
  external_effects: none | bounded | unknown
  business_truth_owner: named-person-or-role
  unresolved_unknowns: [...]
```

若 `mutation_source_fenced=false`，先回第 31 章处理现场；若
`good_after_manifest=unknown`，可以并行恢复候选，但不能批准覆盖或切流。

## 32.1.3 逻辑补偿、对象恢复与整库 PITR 的选择 {#item-32-1-3}

### 三种路线解决不同问题

PostgreSQL 连续归档的物理恢复只能恢复**整个 database cluster**，不是按 database 或
table 选择性恢复
（[官方说明](https://www.postgresql.org/docs/18/continuous-archiving.html)）。
“只恢复一张表”通常意味着先把整套物理副本恢复到隔离环境，再从中逻辑提取对象。

| 路线 | 适合条件 | 主要风险 | 验收重点 |
|---|---|---|---|
| 原地逻辑补偿 | 影响 key 精确、逆变换明确、当前值可条件匹配 | 覆盖并发合法写 | affected-row preimage 与业务不变量 |
| 隔离 PITR 后对象提取 | 只需部分对象的历史值，原服务需继续 | FK、sequence、触发器与跨表遗漏 | 完整依赖与合并 manifest |
| 隔离 PITR 后整库切换 | 影响广、catalog 级破坏、无法可靠逐项修补 | 丢失目标后合法写与外部状态 | 写围栏、delta replay、路由与回退 |

选择依据不是“哪条命令最熟”，而是：

$$
\text{total risk}
  = \text{unknown blast radius}
  + \text{good-after loss}
  + \text{merge error}
  + \text{external inconsistency}
  + \text{cutover risk}
$$

### 什么时候优先逻辑补偿

例如误把一组账户余额减去固定值，且满足：

- 受影响 account ID 由不可变 change ID 精确列出；
- 每行当前版本仍等于错误后的预期值；
- 目标之后的合法变化有独立 ledger；
- 没有不可逆外部副作用，或已经有补偿协议；
- 修补能放在一笔受控事务中，并在 row count 不符时回滚。

应使用条件更新，不要盲目写回：

```sql
UPDATE app.account AS a
SET balance_cents = r.recovered_balance_cents
                  + r.audited_post_delta_cents,
    status = 'active'
FROM recovery_candidate AS r
WHERE a.account_id = r.account_id
  AND a.balance_cents = r.expected_current_balance_cents
  AND a.status = 'mispriced';
```

随后检查实际 row count 必须等于 manifest；少一行说明当前状态已被其他事务改变，整笔
修补应回滚并重新调查。

### 什么时候从恢复副本提取对象

误 `DROP TABLE`、误删一个租户或一个时间分区时，常见流程是：

```text
physical PITR full cluster into isolation
  -> validate historical object and dependencies
      -> export selected schema/data
          -> import into quarantine schema
              -> compare current and recovered identities
                  -> merge under write fence
```

导出前要连同以下对象评审：

- sequence 当前值与 identity；
- foreign key 的父子行；
- partition、index、constraint、trigger、policy；
- large object 与外部文件；
- extension 类型、collation 与函数依赖；
- logical replication identity 和 downstream 消费位置。

单独 `pg_dump -t target` 能生成表数据，不代表它已经捕获业务闭包。先定义 closure，再
导出。

### 什么时候考虑整库切换

下面情况更可能需要整库路线：

- 大范围 schema/catalog 破坏，受影响对象无法可靠枚举；
- 大量相互依赖的数据都被错误转换；
- 目标后写入已被严格围住，或能从独立日志完整重放；
- 业务明确接受目标后的数据损失；
- 原集群已不可信，但恢复候选通过完整引擎与业务验证。

即便如此，也先做 side restore。直接覆盖 managed PGDATA 会同时销毁现场、当前合法
写和比较基准；空间不足不是自动授权覆盖，而是需要事故指挥人明确选择保存哪些证据并
承担什么损失。

### 五个常见伪方案

```text
在 replica 上查旧值
  错误 WAL 通常已经重放；lag 不是受控历史边界。

对已提交事务执行 ROLLBACK
  ROLLBACK 只能影响当前未提交事务。

从最近 full backup 直接启动
  缺少目标前 WAL 时，它只代表 backup 一致点，不代表事故边界。

恢复到“报警前一分钟”
  报警时间不是 commit 时间，时钟与采集链还有误差。

在原 PGDATA 上反复试 target
  每次都覆盖当前现场，丧失比较和回退能力。
```

### 交给下一节的恢复假设

完成本节后，应该得到而不是猜到：

```text
route                  compensate | extract | full-cutover
bad commit             exact identity or bounded interval
inclusive expectation  damage must be present or absent
exclusive expectation  safe facts present, damage absent
good-after             manifest plus replay/merge owner
external effects       fenced and reconciled plan
production cutover     still not authorized
```

下一节把这些业务边界映射到 PostgreSQL 的 time、XID、LSN、name、inclusive 与
timeline 语义。

---

[返回本章目录](../) · [下一节：恢复目标与时间线](../02/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
