你让编码智能体把 users.name 改成 full_name。它更新了模型,改好了每一条查询,然后写了一个只有一行的迁移:ALTER TABLE users RENAME COLUMN name TO full_name;。diff 很干净,测试也过了——因为测试只跑了一个版本的代码。而生产环境跑的是两个。
![]()
Kubernetes 的 Deployment 滚动发布时,会先把新的 ReplicaSet 扩容,同时把旧的缩容,maxUnavailable 和 maxSurge 默认都是 25%。在整个发布窗口里,旧 Pod 和新 Pod 是同时在对外提供服务的。你的迁移只跑一次,就在这个窗口之前或者窗口之中的某个位置。改名之后还活着的每一个旧 Pod,都会把 SELECT name ... 发给一张已经没有 name 列的表。
一行 RENAME 里其实塞了两种失败
第一种是代码与 schema 不匹配。先跑改名,旧 Pod 挂;先发代码,新 Pod 在迁移落地前挂。没有任何一种顺序能让两个版本同时满意,所以 strong_migrations 的 README 说得很直白:重命名一个正在被使用的列,会导致应用报错。Squawk 这个 linter 专门有一条 renaming-column 规则,也是同一个原因。
第二种是锁。改名不碰数据,Postgres 文档写的是对已存储的数据没有影响。但 ALTER TABLE 会拿 ACCESS EXCLUSIVE 锁,除非另有说明,而这种锁模式与其它所有锁模式都冲突,包括一条普通 SELECT 拿的 ACCESS SHARE 锁。如果有一个长事务正占着这张表,改名就得等。而在它等待期间,后面每一条需要这张表锁的语句都会排在它后面。表在改名完成或放弃之前,读写全被堵住。
解法是 2014 年就有的
Danilo Sato 在 2014 年把它叫做 Parallel Change:先把接口扩展成同时支持新旧,迁移客户端,再收缩回只留新的。落到 Postgres 的列改名上,就是四步。
- 扩展:加新列,加同步触发器,让新旧两列保持一致。
- 回填:用独立的批量脚本按主键把老数据搬过去。
- 切换:发新代码,只读写新列。
- 收缩:确认没有已发布代码再读旧列之后,单独一次发布删掉旧列。
回填脚本要按主键分批,每一批都远低于 30 秒的 statement_timeout。某一批超时了,它会自己回滚,循环重试同一段范围。跑完之后要检查有没有漂移:SELECT count(*) FROM users WHERE full_name IS DISTINCT FROM name; 这个数必须是零,才能往下走。
如果这一列需要 NOT NULL,Postgres 18 允许你在不加长锁的前提下补上。ALTER TABLE 文档允许在非空约束上用 NOT VALID,校验作为单独一条语句跑。校验那条的 statement_timeout 比别处长,因为它要扫全表,而且它只拿 SHARE UPDATE EXCLUSIVE 锁,读写照常。
第四步要等到没有任何已发布代码读 name,并且已经过了你会回滚的那个点。然后它在自己的一次发布里删掉旧列。DROP COLUMN 也很快,Postgres 不会物理删除这一列,只是让它对 SQL 不可见,空间会在下一次表重写时回来。有一个坑:有些 ORM 在启动时缓存列清单。strong_migrations 提到 Active Record 会在运行时缓存数据库列,所以在 Rails 上你要先把这一列加进 ignored_columns 并发布,再去执行删除。
该给智能体的规则
智能体写出那一行,是因为没有任何东西告诉它代码还有第二个版本。那就告诉它。Claude Code 在处理匹配文件时会加载 .claude/rules/ 下按路径生效的规则,所以这个文件只在智能体碰到迁移时才出现。规则可以写成:永远不要对运行中的应用正在读的表做 RENAME COLUMN 或 RENAME TABLE,改为提出 expand-contract;扩展迁移用 ADD COLUMN 加一个 BEFORE INSERT OR UPDATE 触发器保持新旧列同步;回填是独立的批量脚本,按主键推进,绝不放进迁移里;每个迁移文件开头都要有 SET lock_timeout 和 SET statement_timeout;DROP COLUMN 单独一个迁移,放在更晚的一次发布里,等没有已发布代码再读这一列之后;被要求做重命名时,写出完整的四步计划,本次改动里只放第一步。
仓库里其它编码智能体读的是 AGENTS.md,所以把这些条目也复制过去。规则文件只是建议,文档说得很直接:Claude 把它们当作上下文,而不是强制配置。所以要用一个不关心 SQL 是谁写的检查来兜底。在 CI 里跑 squawk 'migrations/*.sql',或者在 pull request 上用 squawk-action。它的 renaming-column 规则能抓住那一行,require-lock-timeout 和 require-statement-timeout 能抓住忘了写超时的迁移。它也会对第四步的删除报警,这没关系——那正是应该由人过一眼再发的那一个迁移。
想把 expand-contract 的记账工作自动化,pgroll 会帮你做加列、回填和同步触发器,并通过视图同时提供新旧两个 schema 版本。但手工知道这四步,仍然是你能审阅它、或者审阅你的智能体即将执行什么的前提。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
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.