可以撤销的 schema 变更
一句话总结
可逆性不是代码自身的属性,而是代码、数据与时间的组合。同一项变更,在无人使用时可以回滚;积累一百万条数据后就可能无法回滚。
为什么这是个问题
“有 down 脚本,所以安全”很危险。用于回滚 DROP COLUMN 的 down 只能重建列结构,无法找回列中原有的值。schema 回来了,数据却不会回来。
所以制定回滚计划时,不应只问“有没有回滚脚本”,而应问“回滚后会失去什么”。如果在部署前提出这个问题,通常会选择把变更拆成扩展 → 迁移 → 收缩。
可逆性不是代码的属性
“这项变更可以回滚吗?”准确答案如下。
可逆性是代码、数据与时间的组合。 同一个代码变更,尚无人使用时是可逆的; 积累一百万条数据后,就会变得不可逆。
新增一列能否回滚?昨天刚部署且无人使用时可以。一周内已有一百万条记录写入该列后,DROP 就是在丢弃一百万份数据。schema 能回退,数据不能。
“有 down 脚本,所以安全”这一误解正源于此。回滚 DROP COLUMN 的 down 只能恢复列结构,无法恢复值。因此有句话说:down migration 大多是谎言。
危险 DDL 清单
| DDL | 为什么危险 |
|---|---|
添加 NOT NULL |
为检查所有行而长时间锁表;存在 NULL 就失败 |
| 列改名 | 看似原子操作,本质上相当于删除 + 新增;旧版代码会立即损坏 |
| 类型变更 | 部分 DBMS 会重写整张表,大表耗时很长 |
| 创建索引 | 大表可能产生锁或负载,需确认在线选项 |
| 大规模回填 UPDATE | 巨型事务导致锁与复制延迟,只读副本可能崩溃 |
DROP COLUMN |
数据消失,无法回滚 |
尤其要小心大规模回填。一次事务 UPDATE 五百万行,会让事务日志暴增并产生复制延迟。如果查询流量走副本,查询服务此时会展示旧数据。因此,回填必须分批。
-- 나쁜 예
UPDATE ORDERS SET DLVR_STS = '01' WHERE DLVR_STS IS NULL;
-- 좋은 예: 1000건씩, 사이에 잠깐 쉬면서
UPDATE ORDERS SET DLVR_STS = '01'
WHERE ORD_NO IN (SELECT ORD_NO FROM ORDERS WHERE DLVR_STS IS NULL LIMIT 1000);
-- 영향 행이 0이 될 때까지 반복
扩展 → 迁移 → 收缩(Expand / Migrate / Contract)
这是可回滚 schema 变更的标准模式。核心规则是:一次部署不要同时包含两个阶段。
[1차 배포 — 확장]
새 컬럼 추가 (NULL 허용, 기본값 있음)
애플리케이션: 새 컬럼과 옛 컬럼에 모두 쓰고, 읽기는 옛 컬럼
→ 이 시점에서 롤백하면? 새 컬럼을 아무도 안 읽으니 안전
[2차 — 이관]
기존 데이터 백필 (배치로 쪼개서)
검증: 두 컬럼 값이 일치하는가
[3차 배포 — 전환]
애플리케이션: 읽기를 새 컬럼으로
→ 롤백하면 옛 컬럼을 읽는데, 계속 써 왔으니 값이 있다. 안전
[4차 배포 — 축소]
애플리케이션: 옛 컬럼 쓰기 중단
충분한 관찰 기간 후 옛 컬럼 DROP
→ 이 시점에서야 비가역이 된다
看起来很慢,但这一模式的全部价值就是每个阶段都能回滚。一次性改列名只需 30 秒,失败时服务会停止;这个模式可能持续两周,却不会在任何时点停服。
变更管理台账
SI 项目中的 DDL 不能由开发人员随意执行。应维护变更管理台账,生产变更必须经过批准。
| 项目 | 为什么需要 |
|---|---|
| 变更 ID/日期 | 追踪单位 |
| 目标对象 | 确定影响范围的起点 |
| DDL 脚本文件 | 实际执行内容 |
| 回滚脚本文件 | 没有就不批准 |
| 预计耗时 | 估算服务中断时间 |
| 受影响系统 | 需要通知的集成方 |
| 批准人 | 责任 |
台账的核心价值是强制要求回滚脚本。编写回滚脚本时,会在部署前发现“这项变更无法回滚”,从而推动改用扩展—迁移—收缩设计。
schema 变更与应用部署顺序
这里也经常出错。
- 新增列:先数据库,后应用(应用不能引用不存在的列)
- 删除列:先应用,后数据库(先让应用停止使用,再删除)
一句话:增加时数据库先行,删除时应用先行。
滚动部署期间,旧版与新版会同时运行。因此 schema 必须始终保持两种版本都能工作,这正是扩展—迁移—收缩的根本理由。
蓝绿部署不能解决 schema 问题
讨论零停机部署时,必须指出一点。
计算资源很容易复制,数据库通常仍然共享。 蓝绿部署可以让应用回滚缩短到秒级, 却完全没有解决 schema 问题。
如果蓝、绿环境访问同一个数据库,schema 就必须同时兼容两版应用,最终仍回到同一个结论。
迁移验证
应用变更后要分四层验证。
1. 스키마 컬럼·타입·제약·인덱스가 의도대로인가
2. 데이터 행 수 · 합계 · NULL 개수 · 체크섬이 보존됐는가
3. 성능 주요 쿼리의 실행계획과 응답시간이 나빠지지 않았는가
4. 앱 핵심 기능 스모크 테스트
第 2 项中的校验和尤其有用。只比较行数与总和,无法发现“少了一条,另一条翻倍”的情况。
-- 이관 전 기준선을 만들어 둔다
CREATE TABLE MIG_BASELINE AS
SELECT COUNT(*) AS CNT, SUM(ORD_AMT) AS AMT,
COUNT(DISTINCT CUST_ID) AS CUSTS FROM ORDERS;
-- 이관 후 같은 쿼리로 비교
诀窍是作业前先建立基线。作业后才问“原来有多少条”,已经太晚。
在实际项目中
schema 变更演变成事故,通常有两条路径。
第一是锁。添加 NOT NULL 会扫描全部行并锁表,期间所有使用该表的请求都要等待。在开发数据库中只需 0.2 秒,在一千万行的生产表上可能需要数分钟,这几分钟与服务停机没有区别。
第二是顺序。先部署应用,它会读取尚不存在的列而崩溃;先修改 schema,旧应用又可能违反新约束。因此,“同时部署两者”不是计划;真正的计划,是让无论哪一方先执行都能承受。
也有人试图用蓝绿部署解决,但两种颜色访问同一个数据库这一事实没有改变。应用可以无中断切换,schema 仍然只有一份。