赵杰 · zhaojie.tech
部署窗口里,一个 ALTER 语句把整个发版卡死了:ERROR 1064 (42000),语法错误。脚本在我的库上跑得好好的,到生产 MySQL 直接报错。定位到的那行 SQL 长这样:
ALTER TABLE mem_card_purchase_log
ADD COLUMN IF NOT EXISTS `protocol_version` VARCHAR(32) DEFAULT NULL;
ADD COLUMN IF NOT EXISTS——MariaDB 的方言。它只在 MariaDB 10.x 里存在,MySQL 5.7 没有、8.0 也没有。写脚本的时候图省事顺手用了它(“反正要防重复加列”),语法在 MariaDB 上确实成立,而生产是 MySQL,部署窗当场翻车。这类问题的麻烦在于:方言差异平时完全静默,只在特定数据库版本上引爆,而引爆点往往是最不该出事的生产部署。
全仓治理,不是单点修
一个多年演进的项目,迁移脚本已经积累了 414 个,命名规则是日期加序号(20260723_01_advance_payment_source_txn.sql)。全仓扫描方言命中:8 个文件被 grep 命中,其中 4 个文件 17 处真的用了 MariaDB-only 的 ADD COLUMN / ADD INDEX IF NOT EXISTS,其余是注释里的字面提及(注释留着也会误导后来人照抄,一并改掉)。
修完还要防复发——grep 复测确认零命中之外,更重要的是把”这类 DDL 不允许再进仓库”固化成测试闸:一个行级静态闸测试扫描迁移目录,任何 IF NOT EXISTS 挂在 DDL 动词后的形态直接把构建打红。先让它在旧脚本上跑出 9 处命中(RED),修复后归零(GREEN),证明闸本身是灵敏的(闸首跑 9 处少于 grep 的 17 处:首版闸的正则只盯最典型的形态,随后补齐了覆盖面,最终以全量归零为准——闸比 grep 严不是缺点,漏报宁可补正则,不可放过真违规)。
为什么迁移脚本必须幂等
先回答值不值得为幂等付出这些麻烦。迁移脚本的执行环境不是理想世界:部署执行到一半失败(第七个语句报错,前六个已生效),修复后重跑,前六个语句再执行一遍;多环境推进(测试、预发、生产)里某个环境执行过、某个环境没执行过,脚本必须两边都能跑;私有化交付场景客户库的版本起点不一,脚本往往要面对”半新半旧”的 schema。只要存在任何一种”重跑”可能,非幂等脚本就是把一次性的赌注押在部署永不中断上——而部署恰恰是最容易中断的环节。命名规则(日期加序号)只解决执行顺序,幂等范式才解决”顺序之下每一步都可重放”。
幂等 DDL 范式
MySQL 没有条件 DDL,幂等要自己造。整个仓库统一成一个范式,三步:
-- ① 预检:information_schema 查列/索引是否已存在
SET @col_stx = (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ss_purchase_order_advance_payment_txn'
AND COLUMN_NAME = 'source_txn_id');
-- ② 条件拼 DDL:存在则执行无害语句,不存在则执行真 DDL
SET @col_stx_ddl = IF(@col_stx = 0,
'ALTER TABLE ss_purchase_order_advance_payment_txn ADD COLUMN source_txn_id BIGINT NULL COMMENT ''...'' AFTER payment_account_json',
'SELECT 1');
PREPARE col_stx_stmt FROM @col_stx_ddl;
EXECUTE col_stx_stmt; -- ③ 动态执行
DEALLOCATE PREPARE col_stx_stmt;
-- 执行后核验:列真的在库里
SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'ss_purchase_order_advance_payment_txn' AND COLUMN_NAME = 'source_txn_id';
预检查 information_schema.COLUMNS(列)或 .STATISTICS(索引),IF() 把 DDL 拼成字符串,PREPARE/EXECUTE 动态跑,脚本尾部再 SELECT 一次核验结果。预检在前、核验在后,双侧都做——只预检不核验,DDL 悄悄失败时脚本 rc 仍是 0,污染会静默扩散到下一个环境。目前 414 个迁移脚本里 200 个使用了 PREPARE 范式。
这个范式还有两个变体值得记:
多列不合并。 一次加 4 列时,4 列的存在性分别预检、各自拼一条 PREPARE DDL 只补缺的那列——绝不合成一条 ALTER TABLE:任何一列已存在,Duplicate column 都会把整条语句顶掉,其余列永远加不上。多几条语句换的是任意可重入。
加唯一索引前的数据预检。 幂等不止”对象存在性”一个维度。给已有数据的表补唯一索引,存量重复会让 DDL 失败。所以索引存在性预检之后还有一道重复数据预检:分组计数发现重复(实测存量 46 组合法业务重复)就跳过建索引并 SELECT 报警,把工程决策留给人工,而不是让部署窗炸掉。这个案例给我的启发是:幂等 DDL 的”已存在则跳过”要升级成”前提不满足则跳过并留痕”。
让范式自己守自己
脚本可重复执行,怎么证明?真库双跑:在一个干净的 MySQL 8.0 scratch 库上按序首跑(rc=0、核验断言全过),紧接着原样二跑——rc 仍为 0,两次跑完的 schema 导出做 md5 比对,完全一致才算幂等成立。这套验证不是人肉纪律,而是固化成了 45 个迁移核验测试类:重放幂等、编号规范、字符集、形状漂移、方言可移植性各有专门的闸,迁移目录的任何改动都要过这些闸才能进门。
一句收尾
数据库方言的差异在开发环境最安静、在生产环境最响亮——与其指望写脚本的人永远记得,不如让脚本自己幂等、让测试闸替你记着。