db-migration-review
name: db-migration-review
description: 数据库迁移脚本评审与安全上线。当要写或审一个 DDL 迁移、要给大表加字段加索引、要改字段名或类型、要拆表、要确认迁移能否回滚时使用。触发词:迁移、migration、DDL、加字段、加索引、改表结构、改字段类型、删字段、锁表、pt-online-schema-change、gh-ost、迁移回滚、schema 变更。不负责查询优化(走 mysql-slow-query)和数据同步(走跨库同步类技能)。
数据库迁移评审
迁移是少数「写错了当场就完蛋、而且不一定能撤回」的代码。评审要比普通代码严格一档。
一、硬规矩
先过这几条,任何一条不满足就打回。
- 所有 DDL 走迁移文件,禁止手改数据库。手改的后果是环境之间悄悄分叉,几个月后没人知道生产的真实 schema 是什么。
- up / down 成对,且 down 实际执行测试过。没跑过的 down 不算 down。
- 一个迁移只做一件事。 加字段、建索引、改表名要拆成三个文件。混在一起的迁移,失败时你不知道执行到哪了。
- 已提交的迁移文件不可修改。 需要改就写一个新的。改旧文件会让已经跑过的环境和新环境产生不一致。
- 每张业务表必须有:主键(自增 BIGINT 或 UUID)、
created_at、updated_at。 - 所有外键列必须建索引。 没有的话删父表记录会锁全子表。
- 命名统一:索引
idx_<表>_<列>,唯一索引uk_<表>_<列>,外键fk_<表>_<关联表>。
二、先查现网 schema,别照抄建表文件
这条单独拎出来说,因为它是最常见的翻车原因。
初始建表文件写于项目早期,之后经过了几十个迁移。你以为存在的列可能早就删了,你以为没有的索引可能已经建了。写迁移前必须查一次目标库的真实结构:
SHOW CREATE TABLE <表名>\G
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='<表名>';
SHOW INDEX FROM <表名>;
对多个环境(开发、预发、生产)分别查,不一致就先解释清楚为什么不一致,再动手。
三、按操作类型评审
加字段
一般安全,但注意:
- 新字段要允许 NULL 或有默认值,否则已有行插不进去。
- 不要给大表的新字段设默认值同时又是 NOT NULL,在老版本 MySQL 上会重建全表。MySQL 8.0 的 instant add column 没这个问题,但要确认版本。
- 加完之后,旧版本代码必须仍能工作。这是回滚的前提。
加索引
- 大表加索引用在线 DDL:
ALTER TABLE big_table ADD INDEX idx_big_table_user_id (user_id),
ALGORITHM=INPLACE, LOCK=NONE;
写死 ALGORITHM 和 LOCK,让它在无法在线执行时直接报错,而不是悄悄降级成锁表重建。
- 超大表(千万行以上)或 MySQL 版本不支持时,上
gh-ost或pt-online-schema-change。 - 避开业务高峰。
- 加之前先确认没有冗余:已有
(a,b,c)就不要再建(a)和(a,b)。
改字段名 / 改类型
这是最危险的一类,因为它天然破坏向后兼容。 不能一步到位,必须拆成三个版本:
- 版本 N:加新字段,代码同时写新旧两个字段,读仍然读旧字段。
- 版本 N+1:把历史数据从旧字段迁到新字段,代码切换成读新字段。仍然双写。
- 版本 N+2:停止写旧字段,删掉旧字段。
每一步之间要留足观察时间。急着一步做完,代价是这次发布无法回滚。
改类型同理。扩大范围(INT → BIGINT、VARCHAR(50) → VARCHAR(200))相对安全但仍可能重建表;缩小范围会丢数据,必须先验证没有超范围的值。
删字段 / 删表
- down 迁移必须先备份数据。 只写
ADD COLUMN而不恢复数据的 down,是假 down。 - 删之前先确认没有任何代码引用,包括其他服务、报表、定时任务、BI 看板。全局搜一遍字段名。
- 稳妥做法是先改名成
<字段>_deprecated_20260903观察一个版本,再真删。 - 归到高风险迁移,走人工审批,不随服务启动自动执行。
数据变更(DML)迁移
- 必须带
WHERE。没有WHERE的UPDATE/DELETE一律打回。 - 大批量更新要分批,每批几千行,批间 sleep,避免长事务和主从延迟。
- 执行前后各输出一次受影响行数,写进日志。
- 归到高风险,人工审批。
四、执行机制
- 服务启动时自动执行一次
migrate up,失败即阻止启动,并输出迁移版本、文件名和完整错误上下文。让服务带着未完成的迁移跑起来,是更坏的结果。 - 多实例部署时迁移必须加锁,用数据库锁、迁移工具自带的锁或分布式锁。没有锁,几个实例同时启动会互相踩。
- CLI 要提供
migrate up/migrate down --steps N/migrate status三个子命令。 - 高风险迁移不走自动执行,单独窗口人工执行。
关于 dirty 状态
迁移执行到一半失败,工具会把版本标记为 dirty。此时新旧两个版本的二进制都起不来,回滚二进制是无效的。处理顺序是:
- 先看清楚迁移到底执行到哪一步了,数据库现在是什么状态。
migrate force <正确的版本号>清掉 dirty 标志。- 判断是补齐剩余步骤前进,还是手工回退到上一个版本。
这个流程要写进运维手册,别等出事了现学。
五、上线前确认
- 现网 schema 已实际查过,迁移基于真实结构而非建表文件
- up 在开发库跑通
- down 在开发库跑通,且数据能恢复
- 迁移跑完后,上一个版本的二进制仍能正常工作
- 大表操作评估过锁时间和主从延迟
- 生产执行前,最近一次备份确认可恢复
- 高风险项已人工审批
- 回滚步骤写成了具体命令,不是「回滚一下」
最后一条最容易糊弄过去。回滚预案要具体到可以复制粘贴执行。