学习使用 Claude Code 为 PostgreSQL 迁移编写安全的扩展-收缩变更、回填和回滚计划,并了解在上线前在预发布环境需要核对的内容。

一个 PostgreSQL 模式更改看起来很简单,直到它遇到真实的流量和真实的数据。真正危险的部分通常不是 SQL 本身,而是应用代码、数据库状态和部署时机不再一致的时候。
大多数失败是实际且痛苦的:一次部署失败因为旧代码触碰到了新列、迁移锁住了一个热点表导致超时激增,或者一次“快速”更改悄然丢失或重写了数据。即便没有崩溃,也可能带来细微的 bug,比如默认值错误、约束损坏或索引未完成构建。
AI 生成的迁移又增加了一层风险。工具可能生成语法正确但对你的工作负载、数据量或发布流程不安全的 SQL。它们也可能猜测表名、忽略长期锁,或因为下迁移很难而轻描淡写地处理回滚。如果你使用 Claude Code 进行迁移,就需要护栏和具体的上下文。
本文所说的“安全”包含三层意思:
目标是让迁移成为例行工作:可预测、可测试且无惊无险。
先定一些不可协商的规则。它们能让模型聚焦,也能防止你发布只在本地生效的更改。
把工作拆成小步骤。模式更改、数据回填、应用更改和清理步骤都是不同的风险点。把它们捆绑会让故障源难以定位,回滚也更困难。
优先做增加性的改动,而非破坏性的改动。新增列、索引或表通常风险低。重命名或删除对象更容易引发故障。先做安全部分,发布应用,只有在确认旧对象未被使用时再移除旧东西。
让应用在一段时间内能兼容两种数据形态。代码应能同时读取旧列或新列。这样可以避免常见的竞态:部分服务运行新版代码而数据库仍为旧状态(或相反)。
把迁移当作生产级代码对待,而不是一个小脚本。即便你在使用像 Koder.ai 这样的开发平台(Go 后端配 PostgreSQL,前端 React 或 Flutter),数据库是被所有服务共享的。错误代价高昂。
如果你想在每个 SQL 请求顶部放一套紧凑规则,可以使用如下要点:
一个实用的例子:与其重命名一个应用依赖的列,不如新增列,慢速回填,部署先读新列再回退到旧列,确认无误后再移除旧列。
Claude 能从模糊请求生成不错的 SQL,但安全迁移需要上下文。把你的提示当作一个小型设计说明:展示现状,说明不能破坏的点,并定义“安全”对你上线的含义。
先只粘贴那些重要的数据库事实。包括表定义以及相关的索引和约束(主键、唯一约束、外键、CHECK、触发器)。如果涉及关联表,也把相关片段一起给出。小而准确的片段能防止模型猜名或遗漏重要约束。
补充真实规模信息。行数、表大小、写入速率和峰值流量会改变方案。"200M rows and 1k writes/sec" 与 "20k rows and mostly reads" 是完全不同的迁移。还要说明你的 Postgres 版本以及在系统中迁移如何运行(单事务还是多步骤)。
描述应用如何使用这些数据:重要的读取、写入和后台任务。例如:"API 按 email 读"、"worker 更新状态"、或"报表按 created_at 扫描"。这些决定了是否需要 expand/contract、功能开关,以及回填的安全度。
最后,对约束和交付物要具体。一个简单结构通常有效:
同时要求 SQL 和运行计划会迫使模型考虑顺序、风险以及上线前需要检查的事项。
expand/contract 迁移模式允许在变更进行中不破坏应用的情况下修改 PostgreSQL。不是一次性切换,而是在一段时间内让数据库同时支持旧形态和新形态。
把它想成:安全地新增(expand),逐步迁移流量与数据,然后再移除旧结构(contract)。这在 AI 辅助工作中特别有用,因为它强制你为中间的混乱做规划。
一个实用流程如下:
当用户可能仍在使用旧版应用时就应使用此模式,包括多实例部署、手机应用慢速更新,或任何可能耗时数分钟到数小时的迁移。
一个有用的策略是规划两次发布。发布 1 做 expand + compatibility,以保证即使回填未完成也不会出问题。发布 2 在确认新代码和新数据就位后才做 contract。
复制下面模板并填入方括号中的内容。它促使 Claude Code 生成可运行的 SQL、验证方法和可执行的回滚计划。
You are helping me plan a PostgreSQL expand-contract migration.
Context
- App: [what the feature does, who uses it]
- Database: PostgreSQL [version if known]
- Table sizes: [rough row counts], write rate: [low/medium/high]
- Zero/near-zero downtime required: [yes/no]
Goal
- Change: [describe the schema change]
- Current schema (relevant parts):
[paste CREATE TABLE or \d output]
- How the app will change (expand phase and contract phase):
- Expand: [new columns/indexes/triggers, dual-write, read preference]
- Contract: [when/how we stop writing old fields and remove them]
Hard safety requirements
- Prefer lock-safe operations. Avoid full table rewrites on large tables when possible.
- If any step can block writes, call it out explicitly and suggest alternatives.
- Use small, reversible steps. No “big bang” changes.
Deliverables
1) UP migration SQL (expand)
- Use clear comments.
- If you propose indexes, tell me if they should be created CONCURRENTLY.
- If you propose constraints, tell me whether to add them NOT VALID then VALIDATE.
2) Verification queries
- Queries to confirm the new schema exists.
- Queries to confirm data is being written to both old and new structures (if dual-write).
- Queries to estimate whether the change caused bloat/slow queries/locks.
3) Rollback plan (realistic)
- DOWN migration SQL (only if it is truly safe).
- If down is not safe, write a rollback runbook:
- how to stop the app change
- how to switch reads back
- what data might be lost or need re-backfill
4) Runbook notes
- Exact order of operations (including app deploy steps).
- What to monitor during the run (errors, latency, deadlocks, lock waits).
- “Stop/continue” checkpoints.
Output format
- Separate sections titled: UP.sql, VERIFY.sql, DOWN.sql (or ROLLBACK.md), RUNBOOK.md
另外两条实用提示:
RISK: blocks writes,并说明何时运行(离峰或随时)。即使是小的模式更改也会造成伤害,尤其是在它们导致长时间锁、重写大表或半途失败时。使用 Claude Code 做迁移时,请要求能避免重写并在数据库赶上来的同时保持应用可用的 SQL。
新增可为空列通常安全。在老版本 Postgres 上,新增带非空默认值的列可能会重写整表。
更安全的做法是两步走:先新增 NULL 列且不设默认,分批回填,再为新行设置默认并在数据清理完成后设为 NOT NULL。
如果必须立即强制默认值,请要求对你所用的 Postgres 版本说明锁行为并给出后备方案。
对于大表的索引,请要求使用 CREATE INDEX CONCURRENTLY,以便读写能继续进行。同时提醒它不能在事务块内运行,这意味着你的迁移工具需要支持非事务性步骤。
对于外键,更安全的路径通常是先以 NOT VALID 添加约束,然后在合适时间再验证。这样可以让初始步骤更快,同时仍然对新写入生效。
当要收紧约束(NOT NULL、UNIQUE、CHECK)时,要求 “先清理,再强制”。迁移应检测并修复不合格的行,然后再启用更严格的规则。
在提示中要紧凑地列出检查点:
回填是大多数迁移痛点出现的地方,而不是 ALTER TABLE。最安全的提示把回填当成受控作业:可测量、可重启且对生产温和。
从一些容易运行且难以争议的验收检查开始:预期行数、目标空值率,以及一些抽样比对(例如对 20 个随机 ID 比较新旧值)。
然后要求一个分批计划。分批能缩短锁时间并减少意外。好的请求指定:
要求幂等性,因为回填会半途失败。SQL 应当能安全地重试而不重复或破坏数据。常见模式是 "只更新 new_col 仍为 NULL 的行" 或使用确定性规则。
还要说明回填运行时应用如何保持正确。如果新写入还在继续,你需要一种桥接方法:在应用中做双写、临时触发器或读回退逻辑,并说明哪种方法在你的场景下安全可行。
最后,把暂停与恢复设计进来。要求进度跟踪与检查点,例如一个小表存储最后处理的 ID,以及报告进度的查询(已更新行数、最后 ID、开始时间)。
示例:你新增 users.full_name,由 first_name 和 last_name 生成。安全的回填只更新 full_name IS NULL 的行,按 ID 范围分批,每次记录最后处理的 ID,并在切换完成前通过双写保证新注册的数据正确。
回滚计划不仅仅是“写一个 down migration”。它包括两个问题:撤销 schema 更改,以及处理在新版本上线期间发生的数据变化。Schema 回滚通常可行,但数据回滚往往不可行,除非你事先做了规划。
明确说明“回滚”对你的变更意味着什么。如果你要删除列或就地重写值,就要求一个现实的答案,例如:"回滚可以恢复应用兼容性,但原始数据若非快照无法恢复。" 这种诚实的说明能让你更安全地决策。
要求清晰的回滚触发条件以便在事故时没人争执。例如:
要求完整的回滚包,而不仅是 SQL:若 DOWN SQL 不安全,提供运行手册;包括停用后台作业、切换回旧读逻辑等步骤。
通常有效的提示模式:
Produce a rollback plan for this migration.
Include: down migration SQL, app config/code switches needed for compatibility, and the exact order of steps.
State what can be rolled back (schema) vs what cannot (data) and what evidence we need before deciding.
Include rollback triggers with thresholds.
在上线前,采集一个轻量的“安全快照”以便比对:
同时明确何时不应回滚。如果你只新增了可空列并且应用在双写,向前修复(热修复代码、暂停回填然后恢复)通常比回滚更安全并且更不容易导致数据漂移。
AI 能很快写出 SQL,但它看不到你的生产数据库。大多数失败来自于提示模糊,模型自行填充缺失信息。
常见陷阱是跳过去当前 schema。如果你不粘贴表定义、索引和约束,生成的 SQL 可能会针对不存在的列,或忽略一个唯一性规则,使回填成为缓慢且高锁的操作。
另一个错误是把 expand、backfill 和 contract 放在同一次部署里。这会丢失逃生舱。如果回填耗时或半途报错,你就被卡在期待最终状态的应用中。
最常见的问题:
一个具体例子:"重命名列并更新应用"。如果生成的计划在单个事务中同时重命名并回填,慢速回填会持有锁并破坏线上流量。更安全的提示会强制要求小批次、明确超时和在移除旧路径前的验证查询。
预发布环境能发现小型开发库中永远不会出现的问题:长时间锁、意外空值、缺失索引、被遗忘的代码路径。
首先,核对迁移后 schema 是否与计划一致:列、类型、默认值、约束与索引。快速浏览不够,一个缺失的索引就能把安全的回填变成慢速灾难。
然后在一个真实数据集上运行迁移。理想情况是使用最近的生产副本并对敏感字段做脱敏。如果不能做到,至少在数据量和热点上模拟生产(大表、宽行、索引密集的表)。记录每一步的耗时,以便在生产中有预期。
一个简短的预发布核对清单:
最后,测试真实用户流程,而不仅仅是 SQL。创建、更新和读取受更改影响的记录。如果采用 expand/contract,确认在最终清理前两种 schema 都能正确工作。
假设你有 users.name 列,存储类似 "Ada Lovelace" 的全名。你想要 first_name 和 last_name,但不能在变更期间破坏注册、个人资料或管理界面。
先做一个安全的 expand:新增可为空的列,保留旧列,避免长时间锁。
ALTER TABLE users ADD COLUMN first_name text;
ALTER TABLE users ADD COLUMN last_name text;
然后更新应用行为以兼容两种 schema。在发布 1 中,应用应在新列存在时读取新列,否则回退到 name,并对新写入同时写入两处以保证数据一致。
接着是回填。运行一个批处理作业,每次更新一小块行,记录进度并能安全暂停。例如:按 ID 升序每次更新 1,000 行 first_name 为 NULL 的用户,并记录更新多少行。
在收紧规则前在预发布验证:
first_name 和 last_name,并仍设置 namename 也能正确显示users 表的基本查询没有明显变慢发布 2 切换为仅读取新列。只有在那之后才应添加约束(如 SET NOT NULL)并删除 name,最好在之后的单独部署中完成。
回滚要尽量平淡无奇。过渡期间应用继续读取 name,回填可停止。如果需要回滚发布 2,将读取切回 name 并保留新列直到系统稳定。
把每次变更当作一个小型运行手册。目标不是一个完美的提示,而是一套强制要求关键细节的例程:schema、约束、运行计划与回滚。
标准化每个迁移请求必须包含的内容:
在运行 SQL 前决定好每步的负责人以防止“大家以为别人做”这种情况:开发负责提示和迁移代码,运维负责生产时机与监控,QA 验证预发布行为和边界情况,一人负责最终的 go/no-go。
如果你在聊天中构建应用,先在对话中列出顺序会有帮助。对使用 Koder.ai 的团队来说,Planning Mode 是记录这些顺序的自然位置,快照与回滚可以在上线过程中降低爆炸半径。
发布后尽快安排 contract 的清理工作,趁上下文还新鲜,不要让旧列和临时兼容代码滞留数月。
一个模式更改之所以危险,是因为应用代码、数据库状态和部署时机不再一致。
常见失败模式:
使用 expand/contract(扩展/收缩) 方法:
这样可以在回滚或逐步发布期间保持旧版和新版应用的兼容性。
模型可能生成语法正确但不适合你工作负载的 SQL,这是额外的风险。
AI 特有的典型风险:
把 AI 的输出当作草稿,要求同时提供运行计划、检查点和回滚步骤。
把迁移依赖的事实都粘贴进来,避免模型胡乱猜测:
CREATE TABLE 片段(以及索引、外键、UNIQUE/CHECK 约束、触发器)这些信息能防止猜测并强制模型考虑正确的顺序。
默认规则:分开它们。
一个实用划分:
把所有步骤捆绑在一起会让故障更难排查和回滚。
优先采用以下模式:
ADD COLUMN ... NULL(不设默认,快速)NOT NULL在某些 Postgres 版本上,添加非空默认值可能会重写整表,如果必须立即添加默认,请要求模型说明锁行为并给出更安全的后备方案。
建议:
CREATE INDEX CONCURRENTLY在验证阶段,包含一个快速检查,确认索引存在且被使用(例如在 staging 中对比 EXPLAIN 计划)。
推荐做法:先用 NOT VALID,随后在可控时机验证:
NOT VALID 添加外键约束,使初始步骤更快且不那么侵入VALIDATE CONSTRAINT 来验证已有数据这可以使新写入仍然被约束,但把昂贵的验证延后到你能观察的窗口内。
良好的回填应当是分批、幂等、可重启的。
实用要求:
WHERE new_col IS NULL)这能让回填在真实流量下存活并可恢复。
回滚目标:尽快恢复应用兼容性,即便无法完美还原数据。
可执行的回滚计划应包含:
通常最安全的回滚是切回旧字段的读取逻辑,同时保留新列直到系统稳定。