安全地改动生产数据
目标
将客户用自然语言提出的变更请求收窄为明确条件,准备回滚路径,在副本上演练回滚,再完成正式应用和对照组验证的完整流程。
为什么重要
在之前的课程中,你掌握的权限实际上只有读取。错误的查询可以重新执行,错误的假设可以删除后重新建立。写入则不同。错误的 UPDATE 能回滚到什么程度,完全取决于事先准备了多少回滚手段。
本练习强制执行的四点都是实际现场规则。将请求语句转成条件前先计数——请求者想象的范围与实际变更范围会出现偏差,在计数前双方都不知道差距有多大。先准备回滚路径——文件副本用于整体回滚,行快照用于说明哪些内容如何发生了变化,因此两者都需要。不能只在口头上说可以回滚——回滚失败通常源于小问题,但如果在正式应用后才发现,这个小问题就不再小。同时统计不应发生变化的对象——没有对照组的验证,即使误改了相邻数据也能通过。
本次请求如下。客户运维负责人以结算截止为由,发来“请将所有 pending 订单改为 cancelled”的请求。基准日期为 2026-08-29。但这 45 笔 pending 订单并非同一类:有些已停滞很久,有些昨天才进入、可能仍在处理中,还有些无法确认客户身份。
本练习使用两个条件收窄范围:创建日期早于 2026-08-15(不包含当天),并且该订单的 customer_id 存在于 customers 表中。
步骤
- 创建
/root/change,将/opt/data/support.sql导入为/root/change/ops.db。从工作副本开始,而不是直接处理客户原始数据。 - 在
/root/change/scope.txt中写四行。request_total=是按请求原样转换后会更改的全部 pending 数量;target=是按上述两个条件收窄后的数量;excluded_recent=是创建日期在2026-08-15之后的 pending 数量;excluded_orphan=是客户不在customers表中的较早 pending 数量。 - 创建文件副本
/root/change/ops.db.bak,并将目标行变更前的值输出到/root/change/backup.csv,表头为id,customer_id,amount,status,created_at。 - 在
/root/change/plan.md中编写计划。需要## 요청、## 대상과 범위、## 영향、## 되돌리는 방법、## 검증 방법、## 중단 기준六个章节,而且## 대상과 범위必须位于## 되돌리는 방법之前。 - 在事务中执行 UPDATE,读取
SELECT changes(),然后执行 ROLLBACK。在/root/change/dryrun.txt中写入changes=,以及回滚后重新统计的after_rollback_pending=。 - 创建
/root/change/rehearsal.db,其内容来自ops.db.bak,应用相同变更,再使用backup.csv恢复。在/root/change/rehearsal.txt中写入applied=、restored_pending=,以及将恢复后的数据库与ops.db.bak逐行比较得到的diff=。 - 使用相同条件对
ops.db正式应用变更,并在/root/change/applied.txt中记录changed=。 - 在
/root/change/verify.txt中写四行:backup_ids_cancelled=、control_pending=、paid_unchanged=、refunded_unchanged=;在plan.md末尾追加## 적용 결과章节,记录验证所得数字。
参考
- 使用
sqlite3 -readonly <파일> "<쿼리>"读取,可以防止误写。 - 若要将多条语句放在同一事务中,请通过标准输入传入:
sqlite3 ops.db <<'SQL' ... SQL。 - 逐行比较时,可以使用
ATTACH连接两个数据库,再通过EXCEPT计数。 - 常见错误 1:第 2 步将
excluded_recent和excluded_orphan合并统计。排除原因不同,就必须分别计数,才能在计划中说明原因。 - 常见错误 2:第 5 步忘记 ROLLBACK 并执行到 COMMIT。这样试运行就变成了正式应用。
- 常见错误 3:第 6 步通过重新编写 UPDATE 语句进行恢复。真实事故中手上剩下的只有备份文件,因此必须确认仅使用该文件也能回滚。
创建工作副本
创建 /root/change,将 /opt/data/support.sql 导入为 /root/change/ops.db。从工作副本开始,而不是直接处理客户原始数据。
客户提供的快照是 /opt/data/support.sql。sqlite3 通过标准输入接收 SQL——如果作为参数传递,只会留下一个空数据库。
统计请求范围与实际范围的差异
在 /root/change/scope.txt 中写四行。request_total= 是按请求原样转换后会更改的全部 pending 数量;target= 是按上述两个条件收窄后的数量;excluded_recent= 是创建日期在 2026-08-15 之后的 pending 数量;excluded_orphan= 是客户不在 customers 表中的较早 pending 数量。
分别使用 COUNT 统计按请求原样转换的条件和收窄后的条件。收窄条件有两个——创建日期,以及该客户是否存在于 customers 中。
建立可回滚状态
创建文件副本 /root/change/ops.db.bak,并将目标行变更前的值输出到 /root/change/backup.csv,表头为 id,customer_id,amount,status,created_at。
使用 cp 创建文件副本,使用 sqlite3 的 .mode csv 创建行快照。快照条件必须与第 2 步收窄后的条件相同。
编写变更计划
在 /root/change/plan.md 中编写计划。需要 ## 요청、## 대상과 범위、## 영향、## 되돌리는 방법、## 검증 방법、## 중단 기준 六个章节,而且 ## 대상과 범위 必须位于 ## 되돌리는 방법 之前。
需要六个章节。“回滚方法”章节必须写明使用什么文件回滚,包括文件路径;“中止标准”章节用于说明看到什么结果时应停止。
不提交地进行试运行
在事务中执行 UPDATE,读取 SELECT changes(),然后执行 ROLLBACK。在 /root/change/dryrun.txt 中写入 changes=,以及回滚后重新统计的 after_rollback_pending=。
使用 BEGIN 开启事务,执行 UPDATE 后用 SELECT changes() 读取受影响的行数,再用 ROLLBACK 结束。写下“已回滚”和实际完成回滚是两回事,因此必须重新计数。
演练回滚
创建 /root/change/rehearsal.db,其内容来自 ops.db.bak,应用相同变更,再使用 backup.csv 恢复。在 /root/change/rehearsal.txt 中写入 applied=、restored_pending=,以及将恢复后的数据库与 ops.db.bak 逐行比较得到的 diff=。
在单独副本上操作,而不是在生产副本上操作。回滚时不要重新编写条件,要读取 backup.csv 进行恢复——真实事故中手上只有该文件。
使用收窄后的条件正式应用
使用相同条件对 ops.db 正式应用变更,并在 /root/change/applied.txt 中记录 changed=。
使用与第 5 步试运行时逐字相同的条件。如果此时想修改条件,就意味着必须重新进行试运行。
连同对照组一起统计验证
在 /root/change/verify.txt 中写四行:backup_ids_cancelled=、control_pending=、paid_unchanged=、refunded_unchanged=;在 plan.md 末尾追加 ## 적용 결과 章节,记录验证所得数字。
同时统计应发生变化和不应发生变化的对象。计划只有记录结果后才算闭环——请追加“应用结果”章节。