把线上搞挂的迁移,几乎从来不是那种一眼就危险的。真正翻车的,是在 dev 跑得绿油油的——因为 dev 表里就 100 行。经典翻车现场有个多数人弄错的细节:ALTER TABLE users ADD COLUMN locale TEXT NOT NULL DEFAULT 'en' 在 Postgres 11+ 上,只要默认值是常量,就是纯元数据操作、瞬间完成;但同一条语句在 MySQL 8 上会把整张 4000 万行的表带锁重写,而在 Postgres 上如果默认值是易变的(now()、gen_random_uuid()),也照样重写。真正危险的变种,是给已有列加 NOT NULL:那会触发一次全表 ACCESS EXCLUSIVE 扫描,整个过程读写全堵。AI 会乐呵呵地把上面任意一条原样写出来,不会告诉你哪个引擎会重写。下面这套流程让 AI 干它擅长的(写 up/down 对、生成回填脚本),同时把 AI 容易错的三件事——可回滚、回填策略、随引擎和规模放大的雷——握在你手里。
TL;DR
让 AI 把 up 迁移、down 迁移、回填脚本作为三个独立产物给你,绝不要合成一坨。逐行读锁风险(规则因引擎而异,见下表)。在生产形态的克隆上跑完整流程,计时,跑 down,再跑一次 up。先发兼容旧 schema 的代码,再跑迁移,最后发要求新 schema 的代码(expand/contract)。在 CI 里用 Squawk 静态检查 SQL,让显而易见的雷在进评审前就被拦下。大表上一条 2 分钟的迁移,留出 30–60 分钟做准备,这个比例是对的。
这篇主要解决什么问题
围绕”schema 变更 + 回填 + 可回滚”三角的迁移流程。怎么 prompt AI 生成 up/down 对、怎么在生产形态的克隆上验证、以及哪一类变更(给已有列加 NOT NULL、还有读流量时丢列)你绝对不能信任 AI 生成的迁移不复核。
这篇适合谁看
每周发 schema 变更的后端工程师、用框架自动生成迁移的全栈(Prisma、Drizzle、Alembic、ActiveRecord)、负责共享数据库的平台团队、偶尔做一次迁移、做之间就忘掉那些坑的独立开发者。
什么时候适合用
加列或加表(最常见,多数安全)。给还有读流量的列改名(需要 expand/contract 模式)。丢列(要等读者停用之后)。从另一张表回填非规范化字段。把一张表拆成两张。10 万行以上、运行时长重要的表上的迁移。
什么时候不建议用
多区域复制拓扑变更——要 ops 评审。合规相关的数据搬动(GDPR 删除、审计日志)——每一行自己读。没克隆可以测的系统。“就在线上跑一条小迁移”——没有这种东西。
开工前
- 准备生产形态的克隆。不一定全量数据——可以采样——但表的行数要对得上。1k 行 200ms 跑完的迁移,4000 万行可能跑 18 分钟。
- 确认你的迁移框架既支持 up 也支持 down。有些团队禁了 down;如果是这样,把回滚步骤作为迁移 PR 的一部分写出来。
- 配套的应用代码同时准备好。schema 迁移几乎从不单独发版——要配合读写新列的代码。提前规划发布顺序。
- 分清哪些迁移需要维护窗口、哪些可以在线跑。加可空列通常在线。加 NOT NULL 或重写唯一约束通常不行。
三角
每条迁移都有三个属性。任一错就要付代价:
- Schema 变更 —— 实际的 DDL。AI 语法写得不错,但它不知道你的引擎版本和行数,所以判断不出哪条语句会锁表。
- 回填 —— 给新形态填数据。AI 能凑合写,但常忘了大表要分批。
- 可回滚 —— down 迁移。AI 能写出语法上的 down,但常写不出正确的 down(丢掉的列连数据一起带不回来)。
到底哪些操作会锁(AI 最常错的地方)
AI 写出烂迁移的最大根源,是默认”加列危险、改约束没事”。真相是因引擎而异。下面这张表请放在评审旁边对着看,数据截至 2026 年 6 月:
| 操作 | Postgres 11+ | MySQL 8 / InnoDB |
|---|---|---|
ADD COLUMN 可空、无默认 | 瞬间,纯元数据 | 瞬间(INSTANT 算法) |
ADD COLUMN ... DEFAULT [常量] | 瞬间,纯元数据 | 整表带锁重写 |
ADD COLUMN ... DEFAULT [易变](now()、uuid) | 整表重写,ACCESS EXCLUSIVE | 整表带锁重写 |
给已有列加 NOT NULL | 全表扫描,ACCESS EXCLUSIVE(用 NOT VALID 两步法) | 带锁重写 |
CREATE INDEX | 锁写——要用 CREATE INDEX CONCURRENTLY | 默认在线,但要盯副本延迟 |
DROP COLUMN | 瞬间,纯元数据(空间稍后回收) | 带锁重写 |
ALTER COLUMN TYPE | 通常会重写(个别是空操作,比如 varchar(50)→varchar(100)) | 带锁重写 |
两条 AI 从不主动告诉你的推论:
- 在 Postgres 上,给已有列加
NOT NULL的安全做法是三步NOT VALID套路,而不是裸的SET NOT NULL:ALTER TABLE users ADD CONSTRAINT users_locale_not_null CHECK (locale IS NOT NULL) NOT VALID; -- 瞬间,不扫表 ALTER TABLE users VALIDATE CONSTRAINT users_locale_not_null; -- SHARE UPDATE EXCLUSIVE,读写照常 ALTER TABLE users ALTER COLUMN locale SET NOT NULL; -- 很快,已验证的 CHECK 给它做了证明VALIDATE CONSTRAINT只拿SHARE UPDATE EXCLUSIVE锁,所以它扫表的同时并发读写还能继续跑。 - 裸的
ALTER TABLE ... ADD CONSTRAINT(外键、check)会拿ACCESS EXCLUSIVE锁,更糟的是,在 Postgres 的 FIFO 锁队列里,排队的读会全部堵在它后面——一次 200ms 的验证,可能把整张表卡到最慢那条在途查询跑完为止。永远拆成ADD CONSTRAINT ... NOT VALID再VALIDATE CONSTRAINT。
具体步骤
- 一句话写目标。“给
users加locale列,默认'en',给 4000 万存量行回填,发读它的代码。“含糊的”加多语言支持”会让范围爆炸。 - 让 AI 把 up/down 对和回填计划作为三个独立产物给你。不要让它合成一坨。
git diff HEAD~1 -- migrations/ # 看 AI 生成了什么 - 对着上面那张锁表逐行读 up 迁移。重点检查:没走
NOT VALID拆分的SET NOT NULL或ADD CONSTRAINT(红灯)、Postgres 上带易变默认值的ADD COLUMN、MySQL 上带任何默认值的ADD COLUMN(重写)、Postgres 上不带CONCURRENTLY的CREATE INDEX(红灯)、ALTER COLUMN TYPE(通常重写)。 - 读 down 迁移。确认真能反掉 up。AI 有时会把
ADD COLUMN的 down 写成DROP COLUMN——回滚时把回填的数据一起销毁。如果数据从别处可恢复,可以;否则 down 要明确报错。 - 验证回填计划。10 万行以上的表,回填必须分批(比如每次 1 万行、每批单独提交、中间加短 sleep)、按主键区间可恢复。AI 倾向给你一条
UPDATE users SET locale = 'en' WHERE locale IS NULL——一个事务里对所有命中行持有行锁,还把 WAL 撑爆。 - 让人看之前先 lint。跑 Squawk(
squawk migrations/*.sql)——这是个 Rust 写的 linter,正好专门标这类问题:不带CONCURRENTLY的索引、加NOT NULL列、阻塞式的约束验证、危险的丢列。接上它的 GitHub Action,每个迁移 PR 都自动过一遍。 - 在生产形态的克隆上跑完整迁移。计时。让应用对着迁移后的克隆跑。再跑 down。再跑一次 up。每步都计时。
- 规划发布:先发能兼容旧 schema 和新 schema 的代码,然后跑迁移,再发要求新 schema 的代码。expand / contract 模式。
能产出真实可用迁移的 Prompt
我需要一条 {框架:Prisma / Drizzle / Alembic / 等} 的迁移。
目标:{一句——想要什么 schema 状态,为什么}
现状:
- 表:{名称},线上约 {N} 行
- 现有 schema:{粘相关 DDL}
产出三个独立产物:
1. UP 迁移 —— DDL。先告诉我目标引擎和版本,再针对那个引擎
标注任何会重写整表或拿 ACCESS EXCLUSIVE 锁的语句。Postgres:
用 CREATE INDEX CONCURRENTLY;给已有列加 NOT NULL 走
ADD CONSTRAINT ... CHECK (col IS NOT NULL) NOT VALID、再
VALIDATE CONSTRAINT、再 SET NOT NULL 三步(不要裸 SET NOT NULL)。
2. DOWN 迁移 —— 必须真能反掉 1。回滚会丢回填数据的,down 应
报错并给恢复步骤,而不是静默销毁数据。
3. 回填计划 —— 如果对超过 10 万行的现有表要回填,给出分批、可
恢复的脚本(不是单条 UPDATE)。每批 1 万行。带进度日志。
不要合成一坨。不要生成 seed 数据。不要"顺便清理"迁移范围外的
内容。
质量检查
- Up 迁移在大表上没有全表锁。已有大列上的
NOT NULL走NOT VALID→VALIDATE→SET NOT NULL三步。Squawk 通过(或每条告警都在 PR 里有意识地豁免说明)。 - Down 迁移真能反掉 up。反掉会丢数据的,down 报错并给恢复步骤。
- 10 万行以上表的回填分批且可恢复。大表单条回填一律拒掉。
- 在生产形态克隆上端到端跑过迁移,含 down、再 up。墙钟时间记下来。
- 要求新 schema 的应用代码在迁移 之后 发,不和迁移同一个 PR。expand / contract 顺序遵守。
- 迁移 PR 描述里一行注:“预计锁时间:< 100ms” 或 “需要 5 分钟维护窗口。“
怎么把这流程沉淀下来
- 保存三产物的 prompt。这是最大的一次性收益——分成 up / down / 回填三份比合并问一份安全得多。
- 别靠脑子记雷点 checklist,把它自动化。CI 里挂 Squawk,每个迁移 PR 都能抓出加 NOT NULL 列、不带
CONCURRENTLY的索引、阻塞式约束验证、危险丢列——它不指望评审者那一刻头脑清醒。 - 维护一份小表格”我们做过的迁移和克隆上耗时”。未来估时锚到真实数字,不靠猜。
- 自动生成迁移的框架(Prisma、Drizzle),不要盲信生成的文件。先过一遍上面的 AI 评审 prompt 再 apply。
建议的操作流程
一句话目标 → AI 出 up/down/回填三产物 → 人逐个审锁风险和数据安全 → 在生产形态克隆上跑 → 全程计时 → 写发布顺序(兼容代码 → 迁移 → 要求代码)→ 发布。4000 万行表加一列,迁移 2 分钟,准备 30-60 分钟。比例对的。
容易踩的坑
- 以为
ADD COLUMN ... NOT NULL DEFAULT 'foo'永远便宜(或永远致命)。Postgres 11+ 上常量默认值是瞬间完成;MySQL 8 上同一条会整表带锁重写;易变默认值两边都重写。信任这条迁移前先搞清你的引擎。 - 对已有大列跑裸的
SET NOT NULL或ADD CONSTRAINT。两者都拿ACCESS EXCLUSIVE并强制全表扫描;在 Postgres 上还会把排队的读全堵在后面。用NOT VALID→VALIDATE拆分。 - 信任自动生成的 down 迁移。尤其
ADD COLUMN,“显而易见”的 down 是DROP COLUMN——把新增数据一起销毁。 - 单条 UPDATE 做回填。大表必须分批、加 sleep。
- 把要求新 schema 的代码和迁移塞在同一个 PR。要么迁移挂了代码也挂,要么迁移过了发布顺序很脆。永远两个 PR。
- 在 100 行的 dev 上测,然后在 4000 万行的线上跑。锁竞争和耗时是非线性的。
- 跳过 down 测试。从没跑过的 down 不工作。
- 还有读的时候丢列。永远两阶段:停读、发布、再丢。
FAQ
- 自动生成迁移的 ORM(Prisma、Drizzle)怎么办?: 拿 DDL 骨架可以;回填和锁风险不可信。apply 前一定要用上面的 prompt 复审。
- 我的框架不支持 down 怎么办?: 那 “down” 就是附在 PR 上的书面恢复步骤。在克隆上测。
- 还有读流量的时候必须丢列怎么办?: 两阶段。阶段一:停读、发布、观察残余读。阶段二:丢。永远不合并。
- AI 能帮写回填脚本吗?: 能,前提是分批 / 可恢复 / 带进度日志这几条约束都给。不给约束,AI 会写一条锁表的单条 UPDATE。
- 在线 schema 工具(gh-ost、pt-online-schema-change)怎么样?: 对大 MySQL 表,它们靠复制到一张影子表来绕开整表重写锁。区别在怎么捕获写:
pt-online-schema-change用触发器(同步,影子表与源严格一致,跑挂了支持--resume);gh-ost无触发器——它读 binlog(需要ROW格式),按副本延迟限流,让你掌控切换时机,但进程挂了就丢了、不能续跑。AI 能起个调用草稿;硬约束(必须有唯一键/主键、gh-ost 不支持外键、盯副本延迟)自己对着工具文档确认。 - 一次发布多条迁移怎么办?: 在克隆上按顺序跑通。迁移不一定可交换。把对的代码变更和对的迁移配对。
- 该用哪个 AI 工具来写?: DDL 任何前沿模型都能写。但要让 agent 读你现有的 schema 文件、对本地克隆跑迁移、再把计时结果读回来,agent 型编码工具比纯模型排名更重要——截至 2026 年 6 月可选 Claude Code(跑 Claude Opus 4.7 / Sonnet 4.6)或 Cursor(Sonnet 4.6、Opus 4.7、GPT-5.5、Gemini 3.1 Pro)。见 Claude Code 对比 Cursor。不管用哪个,因引擎而异的锁评审是你的活,不是模型的。