零停机迁移是兼容性与可用性目标,不是把语句变短之后自动获得的保证。数据变化时,新旧应用可能同时运行,因此需要一起安排表结构、写入行为、回填、读取切换以及恢复方式。删除信息并不会因为前面采用了增量步骤就变得可逆;是否还能回退,取决于旧程序需要的数据是否仍然存在且正确。
取锁等待、行级冲突、资源消耗和应用假设都重要。只改元数据的操作也可能被别的事务挡住;回填没有持有排他表锁,也可能耗尽磁盘吞吐或复制容量。验收不能只测一条语句有多快,还需要观察并发业务、队列等待与恢复条件。
先扩展,再回填,后收缩
扩展阶段引入旧读写方能够容忍的新表示。先部署兼容写入方,再依赖新数据;回填存量行并验证不变量后,才切换读取。收缩必须等旧二进制、后台任务和回退需求都不再依赖旧结构。增加可空列经常有用,但仍需取锁和兼容性检查,不能把任何增加列操作都宣布为绝对安全。
双写必须定义失败与先后顺序。同一数据库事务中更新两列,可以维持一致性;分开提交则可能只成功一半。仍然只更新旧列的老写入方,会让已经回填的新列再次过时。因此需要先退役这些写入方,或提供经过验证的同步机制,再宣布已经收敛。回滚只有在它所依赖的旧表示仍然准确时才安全,不是打开双写就自动保证不丢数据。
回填应采用可重启的小批次和稳定进度键。需要防止旧快照覆盖并发应用刚写入的新值,例如事务内检查版本号,或使用符合业务不变量的条件更新。仅检查新列为空并非通用答案,它是否足够取决于写入行为。跳过的行和并发变化的行还要复查,写入方收敛之后再次验证。分批缩短单次事务,却可能增加总工作量,更不能替代正确的发布顺序。
锁是其中一道失败边界
PostgreSQL 的 ACCESS EXCLUSIVE 会与普通读写冲突。许多 ALTER TABLE 形式即使不用重写数据,也要获取它。普通 UPDATE 则获取 ROW EXCLUSIVE 和行锁,不会因此把所有其他写入方都全局排除。具体冲突取决于命中的行和并发操作;长事务会一直持有相关锁,直到事务结束,而不是语句返回后就全部释放。
排队中的强锁可能继续延迟后来的冲突请求。应识别实际阻塞关系、事务边界与等待事件,不能仅凭哪个事务最老就认定它是原因。SQLite 的回滚日志与 WAL 模式属于不同引擎契约。旧文中的毫秒和页数对比,在本次审阅中没有建立精确基准证据;它们不能当作 PostgreSQL 预测,也不是本轮测量结果。
在较弱锁下验证
以 PostgreSQL 17 为例,只有当前写入方已经满足条件时,才添加合适的 CHECK ... NOT VALID。这个选项延后检查旧行,却立即约束新的插入和更新。随后 VALIDATE CONSTRAINT 用 SHARE UPDATE EXCLUSIVE 扫描,它允许普通数据读写,但会与部分维护和结构操作冲突。因此验证并非完全不取锁,也不是对所有并发活动都没有影响。
若已经验证的 CHECK 能证明该列不会为空,且没有在同一条命令中删除它,SET NOT NULL 可以跳过扫描。这仍不会免除最终修改所需的更强锁,而设置 NOT NULL 本身也不必然重写整表。PostgreSQL 18 扩展了非空约束能力,所以必须按实际部署的大版本查语法,不能把不同版本的规则混在一起。
坏掉的样子
回填刚完成一次,仍存活的旧写入方又把新表示变旧。切读前要监控不一致计数和写入方版本。收缩涉及破坏性删除时,恢复可能依赖备份恢复或向前修复;应在删除数据之前演练,而不是承诺任何阶段都能简单撤销。只有进度百分比到达一百,还不足以证明线上不变量成立。
普通 CREATE INDEX 在构建期间阻止写入。CREATE INDEX CONCURRENTLY 允许普通数据修改,但需要额外扫描和等待,不能放进事务块,失败后还可能留下无效索引。这样的索引可能继续增加更新开销,甚至继续执行唯一性约束。应检查具体状态,再进行获授权的恢复操作,不能把无效索引一概当作没有任何效果。
快速验证
下面保留的是运维检查清单,不是隔离的迁移实验。只应在明确选定的一次性 PostgreSQL 数据库中使用,其中已有自己拥有的 events 表且没有 note 列。先确认连接、search_path、目录可见权限、DDL 权限,以及独立的整条语句超时。第四条命令确实改变表结构,不要直接粘贴到生产数据库。
psql -c "SELECT pid, state, now()-xact_start AS age, left(query,50) FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start;"
psql -c "SELECT pid, mode, granted, left(relation::regclass::text,40) FROM pg_locks WHERE NOT granted;"
psql -c "SELECT indexrelid::regclass, indisvalid FROM pg_index WHERE NOT indisvalid;"
psql -c "SET lock_timeout='3s'; ALTER TABLE events ADD COLUMN note text;"
psql -c "SELECT conname, convalidated FROM pg_constraint WHERE conrelid='events'::regclass;"
活动查询只列出候选事务,不能证明谁正在阻塞;pg_blocking_pids 才提供阻塞关系。未授予的锁也不是所有持锁者清单。lock_timeout 限制每次取锁等待,不限制执行时间或总体影响。单看 convalidated 也不能知道证明了哪个条件。本轮没有运行这些命令;发布兼容性、并发回填、约束状态转换和恢复,仍需获授权且固定版本的夹具验证。
来源
- PostgreSQL:ALTER TABLE,
SET NOT NULL、NOT VALID与VALIDATE CONSTRAINT,已核验 2026-09-16。 - PostgreSQL:CREATE INDEX,并发建索引与失败后留下无效索引的形态,已核验 2026-09-16。
- PostgreSQL:显式锁,哪些命令会取
ACCESS EXCLUSIVE与SHARE,已核验 2026-09-16。 - Martin Fowler:并行变更,扩展/收缩模式的另一个名字,已核验 2026-09-16。
- PostgreSQL 17:ALTER TABLE,特定版本的 CHECK 验证与非空状态转换,已核验 2026-10-18。
- PostgreSQL 17:客户端默认设置,取锁等待超时与语句时长的区别,已核验 2026-10-18。
- PostgreSQL 17:系统信息函数,通过 pg_blocking_pids 查询实际阻塞关系,已核验 2026-10-18。
- PostgreSQL 17:事务隔离,并发更新与条件重新检查,已核验 2026-10-18。