如何设计数据库生产数据修复、人工变更与安全操作
不要把修复当反向 UPDATE;先证明“修的是谁,改回什么,怎么不再误伤”
这一页只解决一个问题: 生产数据库已经被坏脚本、坏迁移或坏批任务写错了,不能整库回滚,只能定向修复时,怎样设计对象范围、旧值证据、dry-run、分批执行和修复后复核。
本来只想把测试租户里的“餐饮”合并到“交通”,脚本条件写宽,影响了 163 个真实租户。最危险的反应是马上写一条反向 UPDATE,因为有些“交通”本来就是正确分类。
bad update:
UPDATE ledger_entries
SET category_id = 'transport'
WHERE category_id = 'food'
AND source = 'imported';
real issue:
no tenant whitelist
no candidate table
no old-value evidence
| 错误反应 | 为什么危险 | 先做什么 |
| 马上反向 UPDATE | 会把本来正确的交通改坏 | 先固化被坏脚本影响的对象集合 |
| 整库恢复备份 | 会覆盖事故后的正常新增账目 | 恢复到隔离库,只抽旧值证据 |
| 客服手工改 | 漏修、错修、无审计,派生报表仍错 | 走候选表、dry-run、分批修复和复核 |
| 问题 | 例子 |
| 坏操作是什么 | SQL、执行人、时间、影响行数、事务日志位点 |
| 修复对象能否固化 | 把 entry_id 写入 repair_candidates,不靠临时 WHERE |
| 旧值从哪里来 | 恢复库、审计表、binlog / WAL、业务事件、导入原文件 |
| 执行时如何避免覆盖新改动 | WHERE current_value = bad_value,不覆盖用户已修正值 |
| 哪些派生数据也被污染 | 缓存、搜索、月报宽表、预算汇总、数据仓库 |
| 是否应该走业务补偿而不是 UPDATE | 流水、支付、余额、权益通常不能直接覆盖 |
repair_candidates
- repair_id
- entry_id
- tenant_id
- bad_value
- expected_value
- evidence_source
- evidence_ref
- status: pending | repaired | skipped | failed
- reason
repair rule:
UPDATE ledger_entries e
JOIN repair_candidates c ON e.id = c.entry_id
SET e.category_id = c.expected_value
WHERE c.repair_id = ?
AND c.status = 'pending'
AND e.category_id = c.bad_value;
吸收点: 修复对象和修复动作要分开。先固化候选对象,再执行 UPDATE;否则 WHERE 条件一变,第二刀更深。
| dry-run 必须输出 | 说明什么 |
| 影响租户、账目、月份、分类分布 | 范围是否符合事故窗口 |
| 修复前后样本 | 旧值、新值、证据来源是否能解释 |
| 反例样本 | 相似但不该修的数据没有混进来 |
| 约束和不变量检查 | 外键、状态、余额、月报口径不会被破坏 |
| 资源和批次计划 | 锁范围、批次数、预计耗时、暂停条件清楚 |
关键边界: 没有可信旧值,就不要批量改回去。宁可标记待人工核验,也不要用想象修复生产数据。
| 样本 | 通过标准 |
| 候选集混入不该修的数据 | 反例样本或当前值校验能拦住 |
| 脚本跑到一半中断 | 按 status 续跑,不重复扩大影响 |
| 用户已经手工改回 | 当前值不等于 bad_value 时跳过,不覆盖用户新值 |
| 主库修好但搜索仍旧 | 派生副本复核能发现并触发重建 |
| 月报分类金额仍错 | 按受影响月份重算,不全量乱跑 |
| 账务流水金额错 | 判断走冲正 / 补偿,不直接覆盖原流水 |
总结: 生产数据修复不是“写反向 UPDATE”。先冻结证据,固化候选对象,找到可信旧值,dry-run 给出样本和反例,再小批次、幂等、可续跑地执行,最后复核主库和派生数据。修复的第一目标不是快,而是不误伤。