一个 schema 以版本化的小块逐步前进。

迁移是可版本化的 schema 变更

数据库的 schema 不是一次性的设计;它随使用它的软件一起演进。与其临时编辑 schema,不如用迁移应用一组命名、有序的变更,让任何环境都能重放。 把 schema 当代码 把 schema 当源代码一样对待:每一条迁移都是一个带顺序、带编号、被命名的小步骤,把库从一个状态推进到下一个状态,整个序列都收进仓库。全新的库从空库开始重放这些步骤直到当前版本,这正是测试建库、临时环境克隆、安装新机器的统一口径;没有人再对着线上实例随手敲一条 ALTER TABLE。schema 也因此从「某个实例才知道的事实」变成有历史、可 review、有 diff 的正式产物。 顺序就是契约 迁移按既定顺序执行,于是任何从零开始的环境都会落在同一个已知状态。这种可预测性就是契约:它让测试能创建全新数据库、让暂存能克隆,让运维升级时不用靠猜。随之而来的铁律是——凡是已经被任何环境执行过的迁移,都不得重写或重排,因为它的校验和或文件名本身就是「已经执行过」的证据。篡改历史,环境就开始分叉,再也无法证明一致性。 危险在不可逆 删除列或表的迁移不可逆,必须谨慎对待。给它们配一张清单:先备份,把 drop 拆成单独的后续迁移,这样能先回滚前面的 schema 改动,最后再面对不可逆的破坏。真正的纪律是「只能向前、但留有退路」——绝不要写一条把维护者锁在门外的迁移。上线前一定要在 staging 上跑一遍这类破坏性迁移,让失败模式在真正投入生产前就变成一件平淡无奇的事。 坏掉的样子 最贵的一种坏法,是把不可逆迁移直接送上生产。在 PostgreSQL 里,删除列在新版本中只是廉价的元数据操作,但修改列类型,或新增带易变默认值的列,会重写整张表,期间持有 ACCESS EXCLUSIVE 锁,读写请求全部排在部署后面。表越大,迁移看起来越像卡死,直到语句超时或值班同学手动终止,留下一个需要手工修复的半成品状态。而 down 迁移并不能撤销数据丢失:删掉邮箱列再加回来,得到的是空列,而不是被删掉的地址。破坏性步骤应当拆成独立的后续迁移,上线前先备份,优先使用扩张再收缩的顺序,先加新结构,再回填数据,最后删除旧结构,让每一次发布都平淡无奇。换句话说,只要迁移涉及删数据,就默认它回不来。 另外两类经典故障是并发双跑和篡改历史。如果两个发布副本在没有任何锁的情况下同时执行迁移,它们可能重复应用同一版本,也可能把版本顺序搅乱。生产级的 runner 因此只允许单个执行者,或者借助 PostgreSQL 咨询锁之类的机制加锁,而 golang-migrate 在失败后会把库标记为 dirty,而不是假装一切正常。重写已经被应用过的迁移,或者合并了两个各自新增版本 7 的分支,都会让历史分叉:校验和对不上,环境之间互相矛盾,唯一安全的修复是再写一条新的前向迁移,而不是改写旧文件。strong_migrations 这类检查工具能在进入评审之前拦下危险操作。 ...

一月 24, 2027 · 4 分钟 · XB Field Notes