AI 编写迁移脚本的评审清单
每一份 AI 写出来的数据库迁移脚本合并前都必须过的评审清单:锁行为、回滚验证、批量大小是否合理、以及在生产级数据量上的 dry-run。我不会合并任何跳过这些关卡的迁移脚本,你也不应该合并。
现场笔记:AI 迁移评审先看数据,不先看 SQL
生成 SQL 可以看起来很合理,却完全不适合生产数据集。我会先要行数估算、最慢的表和回滚演练结果,再逐行读脚本。
AI 迁移入口信息: - 表:invoices,4200 万行 - 回填字段:normalized_status - 预计耗时:staging-scale 测试 38 分钟 - 回滚演练:恢复 shadow table snapshot - 人类责任人:数据平台负责人
为什么 AI 默认写出来的迁移是危险的
训练数据里塞满了类似 ALTER TABLE users ADD COLUMN status VARCHAR(32) NOT NULL DEFAULT 'active' 的例子。看起来无害,但在任何有规模的生产表上,它几乎从来都不安全。在老版本的 PostgreSQL(11 之前)以及我至今还能遇到的每一个 MySQL 版本上,这一条语句就会持有排他锁、重写整张表。模型不知道你的表有多大,也不知道你跑的是哪个引擎。它吐出这条语句,只是因为 Stack Overflow 上大多数答案就是这么写的。
过去半年里,我亲眼见过 AI 生成的迁移脚本干过下面所有这些事:在业务高峰期、对一张 4000 万行的表执行没有 CONCURRENTLY 的 CREATE INDEX;在一张写入频繁的父表上加了外键但没有先声明未验证约束(直接持有 share lock);重命名列时先上线了迁移,老代码还在引用旧列名;以及一条 DROP COLUMN,它的回滚方案字面就是一句注释——"从备份恢复"。这几条全部通过了人工代码评审,因为 diff 看起来都很小。
我实际在用的 10 条评审清单
这不是给会议留档用的那种清单。这是我贴到 PR 描述里、一条一条勾完才会 approve 的那份。
- 锁行为:每条语句会拿什么锁、持有多久、锁在什么对象上?如果评审人答不出来,迁移就还没到可以合并的程度。
- Online DDL 的用法:PostgreSQL 建索引必须加
CONCURRENTLY,加约束要先NOT VALID再VALIDATE。MySQL 8 在可用时必须用ALGORITHM=INPLACE, LOCK=NONE。 - 回滚可逆性:能不能在 5 分钟内、不走备份恢复就把它反回去?如果不能,那就需要一份写下来的事故预案,而不是挥挥手说"没事"。
- 批量大小:任何影响超过约 1 万行的
UPDATE或DELETE,都必须分批处理,并有明确的提交边界。 - NULL/DEFAULT 顺序:先加可空列、分批回填、再加 NOT NULL 约束。绝不要一条语句搞完。
- 建索引策略:要么 CONCURRENTLY,要么挑非业务时间,绝不在热表上阻塞写入。
- 回填计划:显式声明。要有批大小、要有续跑用的 resume key、要有预估耗时。
- 部署顺序依赖:代码先上还是 schema 先上?哪个版本的代码兼容哪个版本的 schema?
- 在生产级数据上测过:在一个行数与生产量级差不超过一个数量级的 staging 库上回放过。
- 监控就位:锁等待告警、复制延迟告警,以及一个值班同学知道怎么拉的 kill switch。
模型反复写出来的 5 个坑
我一直维护着一份 AI 迁移翻车清单,都是我亲手修过的。主要集中在 5 种模式。
坑 1:ADD COLUMN ... NOT NULL DEFAULT ...。在 PostgreSQL 10 及更早版本、在使用旧版 inplace 算法的 MySQL 上,以及某些托管云的变种上,这条语句会持有 ACCESS EXCLUSIVE 锁、重写整张表。在一张 1 亿行的表上,这意味着几个小时的停机。正确的姿势是三个迁移步骤,下面会具体讲。
坑 2:没有 CONCURRENTLY 的 CREATE INDEX。普通的 CREATE INDEX 会在整个构建过程中持有写锁。除非你明确要求,AI 几乎从不加 CONCURRENTLY;而且它永远记不住并发索引构建不能在事务块里跑——这会直接和大多数迁移框架的默认行为冲突。
坑 3:给大父表加外键。ADD CONSTRAINT ... FOREIGN KEY 在做校验时会在被引用的表上持有 share lock。安全的姿势是先 ADD CONSTRAINT ... NOT VALID,稍后再用一条 VALIDATE CONSTRAINT——后者只拿 SHARE UPDATE EXCLUSIVE 锁。
坑 4:重命名的"窗口期"。ALTER TABLE ... RENAME COLUMN old TO new 在数据库里是瞬时的,但在部署的集群里并不是。任何还持有旧 ORM 映射的 pod 都会一直报错,直到重启。模型把 rename 当成原子操作。它不是。
坑 5:不可原谅的 DROP。在大多数引擎里,DROP COLUMN 是物理上不可逆的,除非从备份恢复。当你事故已经处理 30 分钟了,一句"我们有备份"并不能算一个回滚方案。
AI 从头到尾都会漏掉的东西
固定有三样。第一,expand-contract 模式:AI 更偏爱一条原子语句,而不是 expand、回填、contract 这种三步走,因为原子版本 token 更少、看起来更"干净"。它也更危险。第二,部署顺序:读取新列的代码到底应该在 schema 变更之前还是之后上线?AI 会开开心心把迁移的两半都写出来,完全意识不到顺序是有讲究的。第三,分批回填:在几百万行以上的表上,任何一条单语句 UPDATE 都会打爆复制延迟、持有锁到足够被注意到。模型默认还是那一条语句写到底。
一个具体例子:给 1 亿行的表加一个 NOT NULL 列
上个月 AI 针对一张用户行为表给出了这样的建议:
ALTER TABLE user_events ADD COLUMN event_source VARCHAR(64) NOT NULL DEFAULT 'web';
按照我们 dry-run 的计时,在我们的 PostgreSQL 13 集群上,这会锁表大概 40 分钟。我们实际上线的版本,是跨两次部署的三条独立迁移:
-- 迁移 1(配合一次会写入该列的代码部署) ALTER TABLE user_events ADD COLUMN event_source VARCHAR(64); -- 迁移 2(作为后台任务跑,每批 5000 行) UPDATE user_events SET event_source = 'web' WHERE event_source IS NULL AND id BETWEEN :lo AND :hi; -- 迁移 3(回填确认 NULL 为零之后) ALTER TABLE user_events ALTER COLUMN event_source SET DEFAULT 'web'; ALTER TABLE user_events ALTER COLUMN event_source SET NOT NULL;
每条语句的加锁时间都在一秒以内。回填跑了 6 个多小时,没有呼叫任何人。AI 建议的那个版本本来会是一个 Sev-1。
Dry-run 不是可选项
只要迁移涉及超过 1000 万行的表,我就会要求两次 dry-run。第一次跑正向迁移,环境是一个从近期生产快照恢复出来的 staging 库。我会记录锁持有时间、总耗时和复制延迟。第二次跑回滚。是真的执行,不是"读一遍"。真正跑一遍、看它跑完、然后把回滚后的 schema 和迁移前的 schema 逐字节比对。
如果回滚在 staging 上不能干净地执行完,那这份迁移就通不过。没得商量。从没跑过的回滚,就等于不存在的回滚。
Given/When/Then 形式的验收标准
- Given a migration touching a table with more than 10M rows When the migration is reviewed Then a dry-run timing log and a rollback dry-run log must be attached to the PR - Given a migration that adds a NOT NULL column When the review reads the diff Then three separate statements must exist: add nullable, backfill, enforce NOT NULL - Given a migration that creates an index on a hot table When the review reads the diff Then CONCURRENTLY must be present and the migration must run outside a transaction block - Given a migration that adds a foreign key to a large parent When the review reads the diff Then the constraint must be added NOT VALID and validated in a later migration - Given any migration with irreversible operations When the review reads the diff Then an incident runbook with RTO and owner must be linked in the PR
那个"一句话测试"
在我 approve 任何迁移之前——AI 写的也好、人写的也好——我都会问一个问题:这玩意儿能在周五下午上线、并且不呼叫任何人吗?如果答案带任何"但是",迁移就打回去。"应该没事,因为表不大"是一个但是。"我们有回滚方案"也是一个但是。唯一能接受的答案是"能,而且这里就是证明它能的 staging 跑批记录。"
这就是为什么即便周围的代码明显没问题,我还是要逐行看 AI 写的迁移。模型擅长语法,不擅长运营判断;而当你在动生产 schema 的时候,运营判断就是全部工作。
可复制产物:AI 编码评审包
在 AI 生成 diff 进入代码评审前使用。它把提示词范围、允许变更和证据要求合并成一个可审查产物。
AI 编码评审包:AI 编写迁移脚本的评审清单 本次要做的决策: - 确认 AI 只在批准范围内生成变更,并为每条验收标准提供证据。 责任人检查: - 产品责任人: - 工程责任人: - QA 或运维评审: 范围边界: - 本次包含: - 本次不包含: - 仍需确认的假设: 验收证据: - 测试或 fixture: - 日志、指标或截图: - 人工复核步骤: AI 边界:生成变更必须留在书面范围内,每条验收标准都要能找到证据。 评审追问: - 没参加需求会的人还会误解哪里? - 哪个证据能证明这次改动足够安全,可以发布?
旗舰使用路径
这是 Spec Coding 用来承接「AI 生成迁移评审」主题的核心参考页之一。建议把它放到真实工单、PR 或发布评审里使用,而不是只当背景文章阅读。
- 适合从这里开始:AI 助手生成了 SQL、ORM 迁移或回填代码。
- 建议复制:迁移评审清单和生产规模风险表。
- 需要附上的证据:行数估算、锁行为、回滚演练和数据库责任人签字。
- 搭配使用:AI 编码治理 Hub 与 数据库规格生成器。
旗舰页使用路径: - 在计划或评审时打开本文。 - 把对应产物复制到工单或 PR。 - 用自己的系统、责任人和失败模式替换示例值。 - 如果证据行仍为空,就不要进入实现。
二次审阅记录:生成迁移需要更严格证据
这篇文章需要说清一件事:AI 写的迁移脚本不会因为语法整齐就获得信任。它需要演练、耗时估算和数据责任人签字。
不要批准,直到: - 已完成接近生产规模的演练。 - 已附锁等待和运行时长估算。 - 回滚已测试,或明确说明不可回滚。 - 数据责任人接受剩余风险。
编辑复核记录
复核日期:2026-04-29。本次补充了可复用产物,按相关主题 Hub 检查了文章定位,并收紧下一步链接,让页面更像可操作参考,而不是孤立长文。
迁移评审清单
AI 写的迁移需要和人写的一样接受生产评审,还要多问一句:它是不是把规模、锁行为或回滚现实性假设掉了?
下载:ai-migration-review-checklist.md
- 记录表规模、预计触达行数、锁类型和预估运行时间。
- 非空列、字段重命名和数据回填使用 expand-contract 步骤。
- 在接近生产的数据集上跑迁移,再批准时间假设。
- 上线前写明中止条件,包括谁有权停止迁移。
- 附上变更后的校验查询,证明数据形态和应用兼容性。
需要追踪的证据
追踪 dry run 时长、锁等待、批处理失败率、校验查询结果、回滚演练耗时,以及上线期间是否需要人工介入。这些信号比 SQL 看起来是否漂亮更重要。
专题阅读路径
这篇文章归入 API 契约 主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。
继续阅读
编辑说明与免责声明
最近复核:2026-04-29。编辑部检查了示例、内链和可复制评审片段,确保内容更适合真实项目使用。
本文用于软件工程教学与实践参考,不构成法律、税务或投资建议。示例场景用于解释规格方法,不对应真实客户数据。
- 作者信息:Spec Coding 编辑部
- 编辑政策:编辑与事实核查政策
- 联系方式:联系页面