AI 写的迁移脚本跑完才发现回不去,改表删列改类型前该准备什么

2026-07-29

内容截至 2026-07。各数据库的 DDL 行为(是否事务化、是否锁表、是否重建整表)随产品与版本变化很大,本文只给判断方法,具体结论以你所用数据库版本的官方说明为准。

多数人把这类事故归因成「AI 把迁移脚本写错了」,其实脚本大概率是对的——错的是你默认它可逆。 删列、改类型、加非空约束这几类 DDL 天然会丢信息,AI 顺手生成的 down 方法只能把结构还原成原来的样子,还原不了列里已经没有的那些字节。你执行前不做准备,执行后就没有任何工具能帮你,这跟代码回滚是两码事。

先说清本篇和站内两篇相近文章的分工:如果你的问题是代码已经回滚了、但数据库和缓存跟代码对不上,看 回滚后状态不一致;如果是 AI 一次改动摊得太开、迁移文件和业务代码混在一个变更里说不清,看 改动范围失控。本篇只管迁移这一步本身:执行前怎么让它变成可逆的,执行后卡在半路怎么救。

一、先确认你现在处在哪一步

排查顺序的第一件事不是看报错,是定位。同样一句「迁移回不去了」,处在下面三个位置的处置动作完全不同。

第一种,还没执行。你手上是一个 PR 或者一段生成好的脚本,这是最好的位置,往下看第三节就够了,你有全部的选择权。

第二种,执行到一半中断。这是最需要冷静的位置。此时数据库的实际结构既不是旧版也不是新版,而是一个迁移工具自己也不认识的中间态。这时候最常见的错误动作是「再跑一遍试试」——迁移工具的版本表可能没记上这次执行,重跑会把已经生效的那部分再执行一次,然后在某条语句上撞到「列已存在」之类的错误再次中断,中间态更乱。

第三种,执行成功了但结果不对。这里要分清是结构不对还是数据不对。结构不对通常还能补一条新迁移改回来;数据不对,尤其是列已经被 DROP、精度已经被截断,那么能不能救完全取决于你执行前留没留副本。

判断方法很直接:把迁移工具记录的版本号,跟数据库里的实际列定义对一遍。两者一致说明工具认为自己成功了,不一致就是中间态。查实际结构可以用信息模式视图,下面这段在多数支持 information_schema 的关系型数据库上可用;不提供这套视图的数据库(比如用数据字典视图或 PRAGMA 的那几家)请换成它自己的等价查询,具体以所用数据库的官方说明为准:

SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'your_schema' AND table_name = 'your_table'
ORDER BY ordinal_position;

二、现象到成因的判别表

下面这张表按现象查成因,我把生产上实际遇得多的几类列在这里。用法是:先在左边找到最接近你现象的一行,用第三列的方法验一下再动手,别跳过验证直接执行第四列。

现象大概率成因怎么验证处置动作
脚本中途报错,部分表已改部分没改该数据库的 DDL 不在事务里,每条语句执行完就生效了用上面的 information_schema 查询逐表核对实际结构与脚本期望不要重跑,写一条从当前实际结构出发的修复迁移,手工对齐版本表
down 执行成功但数据没回来down 只还原结构,被 DROP 的列数据没有任何副本查有没有备份表、有没有覆盖执行时刻的全量备份或增量日志恢复到一个影子库,从影子库把数据回填到线上
ALTER 迟迟不返回,其它请求跟着堆积表锁或元数据锁被一个未提交的长事务挡住查数据库的活动会话与锁等待视图,找持有锁的会话先处理阻塞源,再决定是否改用在线改表方案
改完类型后接口大面积 500应用层的类型映射跟新类型对不上,或者数值精度被截断抽样比对改前副本与改后数据的关键字段先回滚应用版本止血,数据按副本逐行比对修复
预发跑得好好的,生产上失败两边结构早就漂移了,迁移的基线不一致把两边的 information_schema 导出来做 diff先补基线对齐迁移,再执行本次变更
迁移成功但业务读到一堆默认值新增列带了 NOT NULL DEFAULT,全部历史行被一次性填成默认值;把已有列改成 NOT NULL 时,部分数据库在宽松模式下也会把 NULL 悄悄转成默认值(严格模式则是直接报错)抽查执行时刻之前创建的历史行,并确认这次执行用的是哪种模式从副本里区分「本来就是空」和「被填的默认值」,业务语义上这两者不能混
建唯一索引失败存量数据里已经有重复SELECT key_cols, count(*) FROM t GROUP BY key_cols HAVING count(*) > 1先定规则清洗重复数据,清完再建索引

这张表里有一半的行,验证方法都需要一份「改之前的数据长什么样」。这就是下一节的全部意义。

三、执行前的准备动作,按这个顺序做

第一步,给这次变更定性。 把 DDL 分成两类:可逆的和不可逆的。加列、加索引、放宽类型(比如变长字符串变长、整型变宽)、去掉约束,这些是可逆的,出问题反向做一次就回去了。但可逆有个前提要说清:反向操作能不能成功,取决于这中间有没有写进不满足旧定义的新数据。放宽类型之后新写入了超长的值,收窄就会失败或截断;去掉唯一约束之后业务写进了重复行,再加回来同样会失败。所以「可逆」的准确含义是执行那一刻可逆,间隔越久、写入越多,退路越窄——这也是为什么反向操作要趁早做,别拖到下一个迭代。删列、收窄类型、加非空、加唯一约束,这些是不可逆的,反向做一次只能把结构补回来,补不回内容。改列名要单独说:如果迁移工具最终发给数据库的真是一条原地重命名语句,那它不丢数据、改回来就行;但不少 ORM 的迁移在缺少某个依赖或某种方言下,会把重命名展开成「加新列 + 复制 + 删旧列」三步,这时它就变成一次不可逆变更,中途断掉还会留下两列并存的中间态。所以定性改列名之前,先把工具实际生成的 SQL 打出来看一眼——多数迁移工具都提供只打印不执行的选项,用它,别靠猜。定性完你才知道要不要做后面的准备。

第二步,不可逆的 DDL 一律先留副本。 最省事的做法是整表快照,代价是磁盘和执行时间:

CREATE TABLE users_bak_20260729 AS SELECT * FROM users;

表太大就只留将被破坏的那几列加主键:

CREATE TABLE users_col_bak_20260729 AS
SELECT id, phone, level FROM users;

这两段用的是「建表并把查询结果灌进去」的写法,各家数据库的具体语法不完全一样(有的是 CREATE TABLE ... AS SELECT,有的走 SELECT ... INTO),照抄前先确认你这套的写法,也要确认它会不会把主键、索引、非空约束一并复制过来——多数实现只复制列和数据,约束不带。当副本用够了,当恢复源要留意这一点。

副本表要带日期后缀,要在团队的清理策略里登记回收时间,不然三个月后没人敢删。另外别只依赖数据库层的定期全量备份,你得确认它的时间点确实覆盖你这次执行,并且真的能恢复出来——没演练过的备份不算备份。

第三步,把不可逆改成分步可逆。 这是行业里成熟的做法,思路是把一次破坏性变更拆成若干次只做加法的变更:先加新列,再让应用同时写新旧两列,然后回填历史数据,再把读切到新列,观察一段时间,最后才删旧列。每一步单独发布、单独可回滚,任何一步出问题都只需要退回上一步。代价是这次需求要分几个发布周期完成,收益是全过程都有退路。删列这种事,把「停止使用」和「物理删除」拆成两次发布,基本能挡掉绝大部分事故。

第四步,让 AI 帮你写反向脚本并且亲自读一遍。 AI 生成迁移时通常会顺手补一个 down,你要盯的是它写没写数据恢复。如果 down 里只有 ADD COLUMN,那就是在骗你——结构回来了,数据是空的。你可以直接要求它把这条迁移标记成不可逆并给出前置备份语句,比让它硬编一个假的 down 靠谱。关于怎么核对 AI 给出的说法,可以参考如何核查 AI 的回答里的思路,迁移脚本尤其要一句句读,别整段接受。

第五步,在与生产同构的环境上跑一次全流程。 不是跑得通就行,要跑的是完整链路:正向迁移、应用新版本、造几条业务流量、再执行反向迁移、再回滚应用版本。很多迁移的问题不在 DDL 本身,而在新旧两版应用与同一份结构的兼容期上。相关的验证坑可以看测试全绿线上还是炸

第六步,估锁不估时间。 在大表上做 DDL,真正伤人的不是执行慢,是执行期间别人被挡住。执行前要确认这条语句在你的数据库版本上到底会不会锁写、会不会重建整表。这个结论各家数据库、各个版本、各种列类型都不一样,以你所用数据库的官方说明为准,别拿别人博客里的结论直接套。大表上如果确认会锁,就走在线改表的方案(社区里有成熟的开源工具做这件事,原理都是建影子表加触发器或读日志同步再原子切换),而不是硬上一条 ALTER。

四、已经执行完了,怎么救

按损失可恢复性从高到低排:

结构错了、数据还在,最好救。写一条新的正向迁移把结构改回去,不要试图去改历史迁移文件,改了会让所有人的本地库和线上库分叉。版本表如果记乱了,手工把它对齐到实际结构对应的那个版本号,这一步要在维护窗口里做,做之前把版本表本身也备份一份。

数据被改了、原值可推导,中等。比如整型被误改成更窄的类型但实际值都没溢出、时间戳被批量转了时区,这类可以写一次性的修正脚本反算回来。写这种脚本必须先在影子库上跑通并且比对行数,再上生产,并且分批提交、每批记录受影响主键,方便逐批核。

数据被删或被截断、且没有副本,最差。这时候唯一的路是从数据库的增量日志(各家叫法不同,逻辑都是记录变更流水)恢复到一个独立实例,恢复到执行前的那个时间点,再把需要的那部分数据导回线上。这个操作本身有风险,恢复实例绝对不要直接对外提供服务,只用来当数据源。如果连增量日志的保留窗口都过了,就没有技术手段了,这是一个业务问题不是技术问题,该上报上报。

不管走哪条路,同时要做的一件事是把应用先切回旧版本止血。数据修复过程中还在持续写入新数据的话,你会陷入越修越乱的循环。生产事故的边界判断可以对照生产事故里 AI 的边界

五、什么情况下别再折腾

工程师在这种场景下最容易犯的错是不肯认损失,反复尝试,把一个可控的局面拖成不可控的。下面几条我认为是明确的止损线,这是我的判断,不是什么行业标准:

一是你已经不能准确说出「数据库现在的真实结构是什么」。连当前状态都描述不清就动手,每一次尝试都在增加分支。这时候正确的动作是停手,把实际结构完整导出来,画清楚当前状态与目标状态的差,再决定。

二是修复脚本已经改到第三版还没在影子库上跑通。说明你对成因的判断本身是错的,继续调脚本是在解一个错的题。退回去重新做第二节的验证。

三是恢复窗口正在流逝。增量日志有保留期,副本表可能被清理任务扫走。当你意识到某条恢复路径存在时间窗口,优先动作是先把窗口锁住(暂停清理任务、先把日志或备份复制一份出来),而不是先去修。窗口没了,后面再聪明也没用。

四是影响面已经超出你能兜的范围。涉及资金、身份、授权这类数据,或者已经有外部用户报障,就不该由一个人在终端里继续操作了。该拉人、该走事故流程、该给业务方一个明确的降级方案。

五是「换条路」比修更快。有些场景下,把新功能的开关关掉、让业务先回到不依赖这次变更的状态,比把数据修完整快得多。先恢复服务再慢慢修数据,几乎总是对的顺序。

六、避坑清单

把 down 当成保险。 会踩是因为迁移框架的模板里就有 down 这一栏,AI 会老老实实填满,视觉上让人以为有退路。避免方法是给团队定一条规则:凡是会丢信息的 DDL,down 里必须写明「本迁移不可逆,回退依赖 X 备份」,宁可留空也不要写一个假的还原。

在同一个迁移文件里塞多张表的改动。 会踩是因为需求描述是一句话,AI 就一次性全改了。避免方法是一个迁移只动一件事,中断时爆炸半径小,也更容易判断能不能重跑。补一句和前面「不要重跑」的关系:默认不重跑,只有同时满足三条才可以——你已经核对过实际结构、脚本里每条语句都是幂等写法(带存在性判断的那种)、版本表状态和实际结构一致。三条缺一条,就走写修复迁移那条路。这跟前面说的改动范围失控是同一类问题的两个面。

默认 DDL 在事务里。 会踩是因为很多人的经验来自单一数据库,换一个数据库就不成立了。事务型 DDL 的支持程度各家差异很大,避免方法是执行前明确确认你这套数据库的行为,并且按「不在事务里」来设计中断预案——按最坏情况准备,成本很低。

用 SELECT 的经验去估 DDL 的影响。 会踩是因为平时查询很快,就以为改结构也快。实际上很多 ALTER 会重建整张表并持有锁。避免方法是把 DDL 当成一次批处理任务对待,安排窗口、准备中止手段、提前通知依赖方。

在迁移里写业务逻辑做数据回填。 会踩是因为 AI 顺手就把 UPDATE 写进迁移了,本地几百行数据跑得飞快。生产上几千万行的一条 UPDATE 会长时间持有锁并撑爆日志。避免方法是结构变更和数据回填拆开,回填走独立的分批任务,可中断、可续跑、可观测。

忽略应用与结构的兼容期。 会踩是因为发布流程里应用和数据库不是同时生效的,中间必然存在旧代码遇到新结构、或新代码遇到旧结构的时刻。避免方法是让每一版应用同时兼容变更前后两种结构,这也是分步迁移必须成立的前提。

改动没进版本库。 会踩是因为救火时直接在客户端里敲了几条语句,事后谁也说不清做过什么。避免方法是所有手工操作全程记录到一个可追溯的地方,事后补成迁移文件。手工语句和版本库里的迁移文件对不上,是事后复盘时最难还原的一段。

只在自己机器上验过。 会踩是因为本地数据量小、数据分布干净,唯一约束、非空约束在本地永远不会失败。避免方法是用生产脱敏数据的子集做验证,尤其是那些约束类变更。

收束

数据库迁移这件事,AI 能帮你写对语法,但决定能不能回头的是你在执行前的那几个动作。把 DDL 分成可逆和不可逆两类,不可逆的一律先留副本、一律拆成只做加法的分步变更,这两条做到,剩下的问题都只是麻烦而不是灾难。

执行前过一遍这份自检:

  • 这条 DDL 会丢信息吗?丢的是数据、精度还是语义?
  • 有没有一份能覆盖执行时刻、且验证过可恢复的副本?
  • 反向脚本是真能还原数据,还是只还原结构?
  • 这次变更能不能拆成加列、双写、回填、切读、删列这几步?
  • 新旧两版应用是否都能在变更后的结构上正常跑?
  • 中断了怎么办:查实际结构的语句、对齐版本表的方法、通知谁,写下来了吗?
  • 恢复窗口有多长,谁负责在窗口内做决定?

最后一条最容易被跳过:执行前先想好谁有权拍板「不修了,先降级」。等到真出事的时候再找这个人,通常已经晚了。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。