db-migration-review

Category: Data Risk: Low risk niuwoai/skills CC-BY-4.0

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_atupdated_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;

写死 ALGORITHMLOCK,让它在无法在线执行时直接报错,而不是悄悄降级成锁表重建。

  • 超大表(千万行以上)或 MySQL 版本不支持时,上 gh-ostpt-online-schema-change
  • 避开业务高峰。
  • 加之前先确认没有冗余:已有 (a,b,c) 就不要再建 (a)(a,b)

改字段名 / 改类型

这是最危险的一类,因为它天然破坏向后兼容。 不能一步到位,必须拆成三个版本:

  1. 版本 N:加新字段,代码同时写新旧两个字段,读仍然读旧字段。
  2. 版本 N+1:把历史数据从旧字段迁到新字段,代码切换成读新字段。仍然双写。
  3. 版本 N+2:停止写旧字段,删掉旧字段。

每一步之间要留足观察时间。急着一步做完,代价是这次发布无法回滚。

改类型同理。扩大范围(INTBIGINTVARCHAR(50)VARCHAR(200))相对安全但仍可能重建表;缩小范围会丢数据,必须先验证没有超范围的值。

删字段 / 删表

  • down 迁移必须先备份数据。 只写 ADD COLUMN 而不恢复数据的 down,是假 down。
  • 删之前先确认没有任何代码引用,包括其他服务、报表、定时任务、BI 看板。全局搜一遍字段名。
  • 稳妥做法是先改名成 <字段>_deprecated_20260903 观察一个版本,再真删。
  • 归到高风险迁移,走人工审批,不随服务启动自动执行。

数据变更(DML)迁移

  • 必须带 WHERE。没有 WHEREUPDATE / DELETE 一律打回。
  • 大批量更新要分批,每批几千行,批间 sleep,避免长事务和主从延迟。
  • 执行前后各输出一次受影响行数,写进日志。
  • 归到高风险,人工审批。

四、执行机制

  • 服务启动时自动执行一次 migrate up,失败即阻止启动,并输出迁移版本、文件名和完整错误上下文。让服务带着未完成的迁移跑起来,是更坏的结果。
  • 多实例部署时迁移必须加锁,用数据库锁、迁移工具自带的锁或分布式锁。没有锁,几个实例同时启动会互相踩。
  • CLI 要提供 migrate up / migrate down --steps N / migrate status 三个子命令。
  • 高风险迁移不走自动执行,单独窗口人工执行。

关于 dirty 状态

迁移执行到一半失败,工具会把版本标记为 dirty。此时新旧两个版本的二进制都起不来,回滚二进制是无效的。处理顺序是:

  1. 先看清楚迁移到底执行到哪一步了,数据库现在是什么状态。
  2. migrate force <正确的版本号> 清掉 dirty 标志。
  3. 判断是补齐剩余步骤前进,还是手工回退到上一个版本。

这个流程要写进运维手册,别等出事了现学。

五、上线前确认

  • 现网 schema 已实际查过,迁移基于真实结构而非建表文件
  • up 在开发库跑通
  • down 在开发库跑通,且数据能恢复
  • 迁移跑完后,上一个版本的二进制仍能正常工作
  • 大表操作评估过锁时间和主从延迟
  • 生产执行前,最近一次备份确认可恢复
  • 高风险项已人工审批
  • 回滚步骤写成了具体命令,不是「回滚一下」

最后一条最容易糊弄过去。回滚预案要具体到可以复制粘贴执行。