内容提要
文章讨论数据库迁移在Git中无冲突合并后,于应用阶段产生的两类冲突:显性冲突因环境应用顺序不同导致部分环境部署失败;隐性冲突因`IF NOT EXISTS`只检查名称不检查定义,造成不同环境模式静默分歧。PostgreSQL的事务性DDL可将失败迁移回滚为无操作,但`CREATE INDEX CONCURRENTLY`例外。按分支创建模板数据库可低成本隔离,但无法检测跨分支不兼容。
延伸解读
显性冲突:环境顺序决定谁先崩溃
文章用两个分支分别添加列和重命名表,演示了同一对迁移在不同环境按不同顺序应用时,一个环境成功、另一个环境因表不存在而失败。关键在于,迁移时间戳顺序与分支合并顺序是两套时钟,staging 按可存活顺序应用并通过,并不能保证生产环境同样安全。这种失败虽然响亮,但至少会触发告警,属于可修复的显性事故。
隐性冲突:IF NOT EXISTS 只认名字不认定义
两个分支用相同索引名但不同定义(原始列 vs lower(email))创建索引,由于 IF NOT EXISTS 只检查名称是否存在,四条语句全部报告成功,迁移历史表也记录为已应用。结果两个环境模式实际不同,却没有任何错误或告警。这种静默漂移可能数月后表现为“查询只在生产变慢”,且无法从合并记录追溯,只能靠直接比较数据库模式发现。
事务性 DDL 的边界与例外
PostgreSQL 的 DDL 事务性可将失败迁移整体回滚为无操作,避免半应用状态;而 MySQL 即使有原子 DDL,语句间仍隐式提交,多语句迁移无法作为一个单元。但文章提醒两个限制:CREATE INDEX CONCURRENTLY 无法在事务内执行,恰恰是大表上最易中途失败的迁移;回滚只恢复模式,不恢复时间,已成功但逻辑错误的迁移仍需向前修复。
按分支建库:低成本隔离而非检测
利用 PostgreSQL 的 TEMPLATE 复制数据库,可在约 1.3 秒内为分支创建独立副本,避免共享开发库中废弃实验迁移的累积。但需注意模板库不能被连接,通常保留一个无人直接使用的干净基础库。文章明确,这只能提供合并前隔离和检出后可复现状态,并不能检测跨分支不兼容——两个索引迁移仍会在合并后首次相遇。
Q&A
为什么两个数据库迁移在Git中合并无冲突,却会在应用时导致不同环境部署失败?
因为Git只检查文件文本冲突,而迁移的语义冲突(如一个分支添加列、另一个分支重命名表)在文本上不可见。每个环境按迁移到达顺序应用,而分支合并顺序与迁移时间戳顺序可能不同,导致某些环境先执行重命名再添加列,从而报错“relation does not exist”。
使用IF NOT EXISTS写迁移有什么潜在风险?
IF NOT EXISTS只检查对象名称是否存在,不检查定义。如果两个分支创建同名但定义不同的索引(如一个索引原始列,另一个索引lower(email)),后应用的迁移会静默跳过,导致不同环境模式不一致,且迁移历史表记录为已应用,不会报错。
PostgreSQL的事务性DDL如何帮助处理失败的迁移?有什么例外?
事务性DDL允许将多个DDL语句包裹在事务中,失败时整体回滚,使迁移成为可重跑的空操作,避免半应用状态。但CREATE INDEX CONCURRENTLY不能在事务中执行,因此大型表上最可能中途失败的迁移恰好无法获得此保护。
如何低成本地为每个Git分支创建独立的数据库环境?
在PostgreSQL中,可以使用CREATE DATABASE ... TEMPLATE从模板数据库复制,文件级复制而非重放SQL,速度很快(64MB数据库约1.3秒)。建议保持一个干净的模板库不被直接连接,从它复制出分支专用库,切换分支时删除重建。
为什么说“在staging环境先运行迁移”不能保证生产环境安全?
因为迁移的应用顺序可能因环境而异,staging可能恰好以可存活的顺序应用了迁移而通过,但生产环境可能以相反顺序应用而失败。staging的绿色只证明staging的顺序安全,不证明生产的顺序安全。
MySQL的原子DDL能否避免迁移半应用的问题?
不能完全避免。MySQL 8.0的原子DDL保证单条ALTER TABLE要么全成要么全败,但DDL仍会隐式提交,多语句迁移无法作为一个整体事务,因此半应用迁移仍是基线情况,不像PostgreSQL那样支持多语句事务回滚。