预处理脚本必须隔离测试库与生产库连接,禁止硬编码或导入生产配置,强制校验DB_NAME含_test/_staging,MySQL用SELECT DATABASE()、PostgreSQL用SELECT current_database()断言,并支持空跑输出影响行数。
执行预处理脚本前,必须隔离测试库与生产库的连接
很多团队直接在测试环境里连着生产数据库跑脚本,结果 update 语句没加 where 条件,或者误用了 set sql_safe_updates=0,一执行就全表扫——这不是预处理,是事故预演。
实操建议:
- 所有预处理脚本必须通过独立配置加载数据库连接,
DB_HOST、DB_NAME等参数严禁硬编码,也不许从生产配置文件中require或import - 本地或 CI 中运行前,强制校验
DB_NAME是否匹配预期(比如包含_test、_staging字样),不匹配立刻exit 1 - MySQL 场景下,可在脚本开头加
SELECT DATABASE()+ 断言;PostgreSQL 则用SELECT current_database()
预处理脚本要能“空跑”并输出将要变更的行数
上线前没人真敢直接跑一遍脚本看效果,但又不能只靠肉眼 review SQL。关键不是“能不能跑”,而是“跑之前就知道它动多少数据”。
实操建议:
- 每个写操作(
INSERT/UPDATE/DELETE)前,先用等价SELECT COUNT(*)语句估算影响范围,例如:SELECT COUNT(*) FROM users WHERE status = 'pending' AND created_at - 把统计逻辑封装成函数,如
dry_run_count($sql),并在脚本顶部设$DRY_RUN = true开关 - 输出格式统一为:
[DRY RUN] UPDATE users: 127 rows matched, 42 rows will be updated—— 这比“执行成功”有用十倍
无损发布时,预处理脚本必须兼容双写阶段的脏数据
灰度切流期间,新旧代码可能同时写同一张表,字段含义、默认值、约束都可能不一致。这时候预处理脚本如果依赖某个字段“一定非空”或“值唯一”,大概率崩在 UNIQUE KEY 冲突或 NULL 报错上。
实操建议:
- 避免在预处理中做
ALTER TABLE ... ADD UNIQUE,改用应用层兜底去重,或先CREATE INDEX再人工校验 - 对可能为空的字段,显式补默认值:不要
UPDATE t SET flag = 1,而用UPDATE t SET flag = IFNULL(flag, 0) | 1(MySQL)或COALESCE(flag, 0) | 1(PG) - 批量更新务必分页,用
WHERE id BETWEEN ? AND ?而非LIMIT,防止因并发写入导致漏行或重复处理
同步前校验脚本输出和实际变更必须可追溯
最麻烦的不是脚本失败,而是“看起来成功了,但数据没对齐”。比如预处理把 1000 行状态改了,但其中 3 行触发了触发器又悄悄翻回去,日志里却只写了“Done”。
实操建议:
- 每条 DML 后立刻查
ROW_COUNT()(MySQL)或GET DIAGNOSTICS(PG),记录到临时表或日志文件,路径统一为/var/log/migration/20240520_user_status_v2.log - 禁止用
echo "success"当结果,必须输出结构化内容,例如:{"stage":"preprocess","table":"users","action":"UPDATE","matched":1000,"affected":997,"skipped":3} - 所有日志加毫秒级时间戳,且脚本启动时打印
PID和当前 Git commit hash,出问题时能快速定位是哪次部署、哪个版本引入的偏差
真正难的不是写脚本,是让每次执行都像手术刀一样精准可控——尤其当表有上亿行、字段带 JSON、索引正在后台重建的时候,WHERE 条件多一个括号,JOIN 少一个 ON,结果就完全不可逆。










