阅读提要
一次发布同时改表结构和读写代码,回滚就无从下手。迁移的正确姿势是拆成四步,让新旧版本能同时跑。
为什么迁移会出事
最常见的场景:发布时执行 ALTER TABLE posts RENAME COLUMN title TO name,同时代码改成读 name。
发布过程中,旧版本进程还在跑,它读 title——报错。回滚时,代码回退到旧版,但表结构已经是新的——继续报错。
问题不在 SQL 写错,在「结构和代码同时变」这个动作本身。

四步法:加、双写、回填、清理
以「把 title 改名为 name」为例。
第一步:加新列,可空
ALTER TABLE posts ADD COLUMN name TEXT;
这一步对旧代码完全透明。旧代码继续读写 title,新列空着。
第二步:双写
代码同时写 title 和 name。读仍然读 title。
这一步部署后,新数据在两个列里都有。旧版本进程如果还在跑,只写 title——所以回填要能覆盖这部分。
第三步:回填历史数据
UPDATE posts SET name = title WHERE name IS NULL;
分批执行(每批 1000 行),避免长事务锁表。回填期间双写仍在继续,所以不会漏。
第四步:切读,然后删旧列
代码改成读 name。观察一到两个发布周期,确认没有地方再读 title,再:
ALTER TABLE posts DROP COLUMN title;
到这里迁移完成。整个过程任何一步失败都可以只回滚代码,不需要回滚表结构——这是关键。
为什么值得这么麻烦
因为代码回滚比数据回滚便宜得多。
代码回滚是切一个版本,几十秒。数据回滚需要从备份恢复,可能丢数据,可能几十分钟。
四步法的每一步都保证「旧代码仍然能正常工作」,所以任何时刻出问题,回滚代码就够了。
几个具体场景
加字段:最简单。加可空列,或用默认值。注意大表加带默认值的列,某些数据库会重写整表(MySQL 8 之前),要用在线 DDL 工具。
删字段:先让代码不再读写它,观察一个周期,再删。直接删会让旧版本进程立刻崩。
改类型:加新列 → 双写 + 转换 → 切读 → 删旧列。不要原地 ALTER TYPE,尤其是有数据的情况下。
加索引:大表上直接 CREATE INDEX 会锁表。用 CREATE INDEX CONCURRENTLY(PostgreSQL)或在线 DDL(MySQL)。SQLite 的索引创建会阻塞写,安排在低峰期。
加非空约束:先回填,再加约束。直接加会让已有 NULL 行导致失败。
迁移脚本的管理
三条规则:
- 迁移脚本进版本库,和应用代码一起评审;
- 只前进,不改历史脚本。已经执行过的脚本再改,会导致不同环境状态不一致;
- 每个脚本可重复执行(
IF NOT EXISTS、先查后建),避免部分失败后无法重跑。
一个简单的版本表就够:
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TEXT NOT NULL
);
启动时读已应用的版本,跳过已执行的脚本。不需要复杂的框架。
数据量大的时候
回填几百万行,不能一条 UPDATE 跑到底。分批 + 限速:
UPDATE posts SET name = title
WHERE id IN (SELECT id FROM posts WHERE name IS NULL LIMIT 1000);
循环执行,每批之间 sleep 几十毫秒,给正常请求留出 IO。监控复制延迟(如果有从库),延迟过高就暂停。
一个检查清单
发布前问四个问题:
- 旧版本代码在新表结构下还能跑吗?
- 新版本代码在旧表结构下还能跑吗?(发布中途会遇到)
- 迁移脚本失败了,能直接重跑吗?
- 回滚代码之后,数据还一致吗?
四个都是「是」,这次迁移就是安全的。
迁移与备份的关系
每次迁移前必须有一次可用备份,而且这次备份要验证过能恢复。
原因是迁移脚本的失败模式往往不是「报错退出」,而是「执行了一半」。比如分批回填跑到第 300 批时进程被 kill,前 30 万行已经改了,后面的没有。
这时候重跑脚本必须能继续,而不是从头再来——这也是为什么回填条件要写成 WHERE name IS NULL 而不是按 id 区间。
如果迁移涉及删列或改类型,备份就是唯一的退路。这类操作没有「重跑一次」的选项,一旦数据被破坏,只能从备份恢复。
什么时候该停下来
三种情况应该中断迁移,而不是硬着头皮跑完:
从库延迟持续升高:说明写入压力超过了复制能力,继续会拖垮读请求。
锁等待时间变长:大表上的 DDL 会阻塞所有写操作,等多久取决于你的耐心和用户的不满程度。
错误率上升:迁移本身不应该影响正常请求。如果开始出现 5xx,先停,别赌「跑完就好了」。
停下来不丢人。跑到一半出事才丢人。
你认为这篇文章怎么样?
还没有表情反馈

一起聊聊
一个想法、一点补充,或一句简单的问候。
评论与回复
友善交流,彼此尊重第一句话,留给你。
读完有什么感受?欢迎成为第一个分享的人。