Java 程序员的 AI 进化论 | AI 帮我做 MySQL 表结构升级,三天活变半天
上周业务方甩过来一张改表清单:用户表加 4 个字段、订单表拆两个字段到独立子表、地址表把 phone 列从 varchar(20) 收紧到 varchar(11)、再加 3 张关联表的外键索引。一共 11 张表,37 处改动。按我以前的节奏,写 ALTER、做数据迁移脚本、配回滚 SQL、跑回归验证,三天起步。这次我拉了 AI 一起来做,从 schema diff 到 Flyway 落地,半天搞定。这篇文章记录下整个流程,包括几个坑。
一、为什么这种活最容易翻车
数据库迁移这种事,写代码的老手也不见得能避雷。改业务代码出 bug 大不了回滚发布,改表结构出事是真要命------数据写进去就回不来了。我自己踩过两次大坑:
第一次是把 nullable 字段改成 NOT NULL,没塞默认值,生产环境老数据几万行直接报错,凌晨两点被叫起来热修复。
第二次是给一张 8000 万行的表加索引,没走 ALGORITHM=INPLACE,跑了 40 分钟把从库连接池打满。
这两次教训让我得出一个判断:表结构升级不是写 SQL 的事,是风险控制的事。schema diff 要全、迁移脚本要可回滚、上线要灰度。AI 在这种「步骤明确但细节繁琐」的活上,能帮你把繁琐压下来,但风险判断得你自己兜。
二、整体方案:AI + Flyway 的协作分工
我先定了个分工:人负责风险判断和 review,AI 负责 diff、SQL 草稿、回滚脚本、变更说明。Flyway 负责版本化和落地。流程是:
- 用 mysqldump 把新旧 schema 导出,做 diff
- AI 读 diff,生成 ALTER + 数据迁移 SQL + 回滚 SQL
- 人 review,重点看锁表、默认值、外键、索引算法
- 写成 Flyway Migration 文件,进 git
- 灰度环境跑通,再切生产
依赖配置我贴在下面这张表,不用翻 pom.xml。
| 依赖 | groupId | artifactId | 版本 | 作用 |
|---|---|---|---|---|
| Flyway Core | org.flywaydb |
flyway-core |
9.22.3 | 数据库版本化迁移 |
| Flyway MySQL | org.flywaydb |
flyway-mysql |
9.22.3 | MySQL 方言支持 |
| MyBatis-Plus | com.baomidou |
mybatis-plus-boot-starter |
3.5.5 | ORM 层 |
| Spring Boot Web | org.springframework.boot |
spring-boot-starter-web |
3.2.x | Web 框架 |
整体流程我画了张图,对着图看会更清楚:

三、Schema Diff:让 AI 先看清差异
旧 schema 我用 mysqldump --no-data 导出一份,新 schema 用设计稿(DDL 文本)。两份都丢给 AI 做 diff。这里我用了三个不同模型对比了一遍,结果挺有意思:
| 模型 | diff 召回率 | 漏检字段数 | SQL 可执行率 | 平均生成耗时 |
|---|---|---|---|---|
| DeepSeek V3 | 91% | 3 | 87% | 4.2s |
| 通义千问 Max | 86% | 5 | 79% | 5.8s |
| Claude 3.5 Sonnet | 96% | 1 | 94% | 6.5s |
数据来源是我自己造的 50 张测试表的对比,样本量不大但够看趋势。Claude 漏检最少但慢,DeepSeek 快但漏字段偏多。我末了选了 Claude 做主 diff,DeepSeek 做交叉校验,两边都报的改动才认。
Prompt 模板我固化成了这样:
java
// MigrationPromptBuilder.java - com.example.migration
public class MigrationPromptBuilder {
public String buildDiffPrompt(String oldSchema, String newSchema) {
return """
你是 MySQL DBA。下面是同一组表的新旧 DDL。
请输出结构化 JSON,按表名分组,每张表列出:
- added_columns: 新增字段(含类型、是否 NULL、默认值、注释)
- dropped_columns: 删除字段
- modified_columns: 类型/约束变更(含旧值、新值)
- added_indexes / dropped_indexes
- foreign_key_changes
只输出 JSON,不要解释。表结构如下:
## OLD_SCHEMA
%s
## NEW_SCHEMA
%s
""".formatted(oldSchema, newSchema);
}
}
这一步关键是让 AI 输出结构化 JSON,而不是直接给 SQL。直接给 SQL 你没法 review,结构化之后你能按字段一条条对。
四、生成迁移 SQL:AI 出草稿,人定风险等级
AI 拿到 diff 后,按改动类型生成 ALTER 语句,每条都标风险等级(低/中/高)。这里我又踩了个坑:
坑一:AI 给大表加索引没加 ALGORITHM/LOCK 选项
8000 万行的订单表加复合索引,AI 直接给的是 ALTER TABLE t_order ADD INDEX idx_user_status_create(user_id, status, create_time)。这条语句默认会走 LOCK=DEFAULT,在 MySQL 5.7 上等价于 LOCK=SHARED,会把表锁成只读。生产高峰期跑这条等于自杀。
正确的写法是显式声明:
java
// HighRiskIndexMigration.java - com.example.migration
public class HighRiskIndexMigration {
/**
* 大表加索引,必须显式 INPLACE + NONE 锁
* 5.6+ 支持 online DDL,但默认不一定走 INPLACE
*/
public String buildSafeAddIndex(String table, String indexName, String columns) {
return """
ALTER TABLE %s
ADD INDEX %s (%s),
ALGORITHM=INPLACE, LOCK=NONE;
""".formatted(table, indexName, columns);
}
/**
* 如果 INPLACE 不支持(如修改列类型),用 pt-osc 或 gh-ost
* 这里只生成检查 SQL,实际走 DBA 工具
*/
public String buildFallbackCheck(String table) {
return """
-- 检查当前 MySQL 版本是否支持该 DDL 的 INPLACE
SELECT ALGORITHM, LOCK, COMMIT_TYPE FROM information_schema.ALGORITHM_DEPENDENCIES
WHERE TABLE_NAME = '%s';
""".formatted(table);
}
}
这个坑让我意识到一个关键原则:AI 生成的 SQL 必须按风险等级分类,高危的不能直接进 Flyway。我加了一层风险拦截,高危 SQL 走 DBA 工具(pt-osc / gh-ost),只有低中危才进 Flyway 自动跑。
风险分级我定了个表:
| 风险等级 | 改动类型 | 执行方式 |
|---|---|---|
| 低 | 加列(带默认值) | Flyway 自动 |
| 低 | 删索引 | Flyway 自动 |
| 中 | 改列类型(兼容方向,如扩长度) | Flyway 自动 + 灰度验证 |
| 中 | 加 NOT NULL 约束 | Flyway 自动 + 默认值兜底 |
| 高 | 大表加索引(>1000 万行) | DBA 工具,绕开 Flyway |
| 高 | 改列类型(不兼容方向,如缩长度) | DBA 工具 + 数据回填脚本 |
| 高 | 删列 | 双写过渡,不直接删 |
五、Flyway 落地:版本化 + 回滚脚本
低中危 SQL 进 Flyway,命名按 V{date}__{desc}.sql,回滚按 U{date}__{desc}.sql。Flyway 社区版不支持自动回滚,所以 U 文件是给人手动执行的。
java
// FlywayMigrationRunner.java - com.example.migration
import org.flywaydb.core.Flyway;
import org.springframework.stereotype.Component;
@Component
public class FlywayMigrationRunner {
private final Flyway flyway;
public FlywayMigrationRunner(Flyway flyway) {
this.flyway = flyway;
}
/**
* 跑迁移前先 dry-run,输出待执行 SQL 到日志
* Flyway 9.x 用 repair + validate 模拟
*/
public MigrationResult dryRun() {
MigrationResult result = new MigrationResult();
try {
flyway.validate();
result.setOk(true);
// 待执行脚本列表
result.setPendingScripts(flyway.info().pending());
} catch (Exception e) {
result.setOk(false);
result.setError(e.getMessage());
}
return result;
}
public MigrationResult execute() {
MigrationResult result = new MigrationResult();
try {
int applied = flyway.migrate();
result.setAppliedCount(applied);
result.setOk(true);
} catch (Exception e) {
// 失败立即停,等人工介入执行 U 文件回滚
result.setOk(false);
result.setError(e.getMessage());
}
return result;
}
public record MigrationResult(boolean ok, int appliedCount,
String error, Object[] pendingScripts) {
// 简化的结果记录对象
public void setOk(boolean ok) {}
public void setError(String error) {}
public void setPendingScripts(Object[] scripts) {}
public void setAppliedCount(int count) {}
}
}
坑二:AI 生成的回滚 SQL 漏了数据维度
AI 给的回滚脚本只回滚 schema(ALTER TABLE t_user DROP COLUMN nickname),但生产环境已经用新字段跑了两小时,数据有了,回滚时这些数据就丢了。
正确的回滚策略应该是双写过渡:新字段加上之后,先让代码同时写新旧两个字段,跑一周确认稳定,再把老字段下掉。AI 一开始不理解这个节奏,得在 Prompt 里明确告诉它「删字段必须走双写,回滚脚本只针对加字段的场景」。
我把这条规则加进了 Prompt:
java
// RollbackStrategySelector.java - com.example.migration
public class RollbackStrategySelector {
public enum Strategy {
DROP_COLUMN, // 直接 DROP,回滚用 ALTER ADD(默认值兜底)
DUAL_WRITE, // 双写过渡,删字段延后 7 天
NONE // 不可回滚(数据迁移类,需备份)
}
public Strategy choose(String changeType, boolean hasDataMigration) {
if (hasDataMigration) return Strategy.NONE;
if ("DROP_COLUMN".equals(changeType)) return Strategy.DUAL_WRITE;
return Strategy.DROP_COLUMN;
}
}
六、实测数据:人工 vs AI 辅助
这次迁移我对比了去年的同规模项目(10 张表、30 处改动),耗时差异挺明显:
| 阶段 | 纯人工(去年) | AI 辅助(本次) | 节省 |
|---|---|---|---|
| Schema diff | 3 小时 | 12 分钟 | 93% |
| 写 ALTER SQL | 2 小时 | 25 分钟 | 79% |
| 写回滚脚本 | 1.5 小时 | 10 分钟 | 89% |
| Code Review | 1 小时 | 2.5 小时 | -150% |
| 灰度验证 | 4 小时 | 3 小时 | 25% |
| 总计 | 11.5 小时 | 6.3 小时 | 45% |
注意 review 时间反而涨了 150%------这正是我前面强调的,AI 出的 SQL 必须逐条核风险,这步偷不得懒。整体省的时间主要在写脚本这种重复劳动上。
另一个对比维度是 AI 漏检率随表数量的变化:
| 表数量 | AI 漏检字段数 | 人工二次校验耗时 |
|---|---|---|
| 5 张 | 0 | 8 分钟 |
| 10 张 | 1 | 22 分钟 |
| 20 张 | 3 | 45 分钟 |
| 50 张 | 7 | 2 小时 |
表数越多,AI 漏检越多,人工校验成本非线性上涨。我的判断是:AI 适合 20 张表以内的迁移,超过 30 张就该拆批次。
七、灰度阶段又冒出来三个问题
你以为 review 过了就稳了?灰度环境一跑又出事。这次灰度阶段我额外发现了三个 AI 没主动提示的问题,每个都得手动补。
问题一:外键约束的依赖顺序
AI 一次生成了 11 张表的迁移脚本,但没考虑外键依赖顺序。t_order_item 引用 t_order 的 id,如果先改 t_order_item 加外键,再改 t_order 主键类型,外键创建会失败。Flyway 默认按文件名顺序执行,得手动给文件命名加序号前缀。我把脚本命名规则从 V{date}__{desc}.sql 改成 V{date}_{seq:02d}__{desc}.sql,seq 是依赖序号。
问题二:触发器和存储过程引用了被改的列
老库有个触发器在 t_user 的 phone 列上做格式校验,AI 把列改成 varchar(11) 没动触发器。灰度跑插入测试时触发器报长度溢出。这个 AI 真发现不了,因为它只看 DDL,不看触发器和存储过程定义。补了一个步骤:迁移前导出所有触发器/存储过程定义,grep 被改列名,看有没有引用。
问题三:binlog 格式对 ONLINE DDL 的影响
生产 MySQL 8.0 默认 binlog_format=ROW,部分 ONLINE DDL 操作会走 row-based 复制,从库应用 binlog 时是大事务。一张 5000 万行的表加索引,主库 12 分钟跑完,从库 binlog 应用拖了 35 分钟,延迟告警。这个坑也是 AI 没主动提的------它不知道你的从库拓扑。补救方案是把这种 DDL 拆成小批次,或者错峰执行。
补完这三个洞,灰度才算真正跑通。灰度耗时统计:
| 灰度阶段问题 | AI 是否识别 | 人工补救耗时 |
|---|---|---|
| 外键依赖顺序 | 否 | 25 分钟重命名脚本 |
| 触发器引用变更列 | 否 | 40 分钟导出+grep |
| binlog 大事务 | 否 | 30 分钟拆批次 |
| 合计 | 0/3 | 1.6 小时 |
这张表挺说明问题:AI 在 DDL 本身写得不错,但跨对象依赖(触发器/存储过程/复制拓扑)几乎完全失明。这是它的能力边界,也是人 review 时最该盯的地方。
八、踩坑总结和检查清单
整体跑下来,AI 辅助数据库迁移能用,但必须有人兜底。前面提到的坑再强调一遍:
- AI 不会主动加大表 DDL 的
ALGORITHM/LOCK参数,高危 DDL 必须走 DBA 工具,不进 Flyway - AI 的回滚脚本只考虑 schema,不考虑数据,删字段必须走双写过渡
- AI 看不见跨对象依赖(触发器、存储过程、复制拓扑),灰度阶段必须人工补一遍
下面这张清单是我每次迁移前必走的流程:
| 检查项 | 建议 |
|---|---|
| Schema diff 是否双模型交叉验证 | 用两个不同模型做 diff,两边都报的改动才认 |
| 大表(>1000 万行)DDL 是否标高危 | 高危走 pt-osc / gh-ost,不进 Flyway |
| ALTER 语句是否显式声明 ALGORITHM/LOCK | 不声明就按默认走,可能锁表 |
| 新增 NOT NULL 字段是否带默认值 | 不带默认值老数据直接报错 |
| 删字段是否走双写过渡 | 直接删会丢生产数据,至少双写 7 天 |
| 回滚脚本是否覆盖数据维度 | 只回滚 schema 不够,数据也要能回 |
| Flyway 文件命名是否含日期 | V20260817_01__desc.sql,避免冲突 |
| 灰度环境是否完整跑过一遍 | 灰度不通不要上生产 |
| 是否备份了迁移前的表结构 | mysqldump --no-data 留一份,出事能恢复 |
| Code Review 是否逐条核风险等级 | 每条 SQL 都要标低/中/高,高危不进 Flyway |
这套流程我跑了一个月,五次迁移零事故。AI 没让我变成甩手掌柜,反而让我把精力从「写 SQL」挪到了「判风险」上------后者才是数据库迁移真正的核心能力。如果你也打算把 AI 引入迁移流程,我建议先从低风险的纯加字段场景试起,跑顺了再扩展到改列、删字段这种高风险操作。别一上来就让它处理核心业务表,翻车成本太高。