生产环境的数据库表结构变更,向来是后端开发最头疼的事之一。直接改?轻则锁表,重则服务中断。Azure SQL 开发专栏最近发了一篇实操向文章,把这件事拆成了六个阶段,核心思路是一个叫「扩展-收缩」的模式——先加新结构,等应用完全迁移过去,再删旧结构。
文章用了一个非常具体的例子:把 Name 列拆成 FirstName 和 LastName 两列。听起来简单,但真正难的不是写那条 ALTER TABLE 语句,而是变更的顺序。作者 Jerry Nixon 的原话是:"单个 SQL 语句很容易。真正的纪律在于变更顺序。"
![]()
六个阶段,一步都不能跳
整个流程被拆成六个明确的步骤,每一步都有它存在的理由:
- 新增列:先加两个可为空的新列,旧结构保持原样,此时没有任何破坏性操作。
- 应用层双写:代码里同时往旧列和新列写数据,保证新旧结构数据同步。
- 分批回填:历史数据不能一次性 UPDATE,得按批次填充新列,避免长时间锁表。
- 切换读取:通过金丝雀发布或功能开关,逐步把读请求切到新结构上。
- 停止旧列写入:确认所有消费者都迁移完成后,代码里不再写旧列。
- 删除旧列:最后一步才是 DROP,而且删之前还得先观察一阵子。
这套顺序的核心逻辑是:任何时刻,新旧结构都是共存的。你永远不会执行一个"破坏性跃迁"——比如直接删列或者改类型——因为那必然导致依赖方报错。
为什么应用层双写比触发器靠谱
文章里专门讨论了一个取舍:同步逻辑放哪?作者明确建议,在可行的情况下优先放应用层。原因很实际:应用层的双写逻辑可见、可调试,迁移完成后删掉也容易。触发器或存储过程虽然能"自动"同步,但它们是隐形的,出了问题很难排查,而且迁移结束后还得记得清理。
当然也有例外——如果多个应用直接写同一张表,或者写入路径本来就走存储过程,那触发器或存储过程反而是更合适的选择。这个判断得看你的实际架构。
回填是最大的坑:别用一条大 UPDATE
很多人会想:历史数据不多,一条 UPDATE 搞定不行吗?文章明确警告:单条大体积 UPDATE 会长时间持锁、撑大事务日志,给生产环境造成巨大压力。给出的替代方案是一个基于 WHILE 循环的分批更新模式,每次处理 TOP(1000) 行,配合双写机制,确保回填窗口期间新写入的行已经是正确数据。
这个细节很关键——回填不是"闷头跑一次",而是要和正在进行的双写并行,分批推进,把锁竞争控制在最小范围。
删列之前,先搞清楚谁在用旧列
最后一步 DROP 之前,文章提醒了一个容易被忽略的隐患:报告、ETL 作业、脚本和下游服务可能还在依赖旧列,而主应用根本不知道。作者建议用 Query Store、应用日志和 Extended Events 去发现这些"隐蔽消费者"。在不可逆的删除之前,把旧列保留为只读废弃状态,是一个安全缓冲。
这个建议很实在——你永远不知道哪个角落的报表还在 SELECT 那个列名。删了,就是事故;留着,最多是代码洁癖难受一点。
一句话总结
表结构变更不可避免,挑战不在于避免它,而在于不中断服务地完成它。扩展-收缩模式不是银弹,但它提供了一套可执行的纪律:先扩展、再迁移、后收缩。顺序对了,风险就控住了。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.