1 分钟

Claude Code PostgreSQL 迁移:用于安全变更的提示

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

Claude Code PostgreSQL 迁移:用于安全变更的提示

什么让 PostgreSQL 模式更改变得危险

一个 PostgreSQL 模式更改看起来很简单,直到它遇到真实的流量和真实的数据。真正危险的部分通常不是 SQL 本身,而是应用代码、数据库状态和部署时机不再一致的时候。

大多数失败是实际且痛苦的:一次部署失败因为旧代码触碰到了新列、迁移锁住了一个热点表导致超时激增,或者一次“快速”更改悄然丢失或重写了数据。即便没有崩溃,也可能带来细微的 bug,比如默认值错误、约束损坏或索引未完成构建。

AI 生成的迁移又增加了一层风险。工具可能生成语法正确但对你的工作负载、数据量或发布流程不安全的 SQL。它们也可能猜测表名、忽略长期锁,或因为下迁移很难而轻描淡写地处理回滚。如果你使用 Claude Code 进行迁移,就需要护栏和具体的上下文。

本文所说的“安全”包含三层意思:

  • 向后兼容:旧版本和新版本的应用在上线过程中都能运行。
  • 可观测:你可以衡量进度并快速发现问题。
  • 可逆:你有在压力下可执行的回滚计划。

目标是让迁移成为例行工作:可预测、可测试且无惊无险。

在编写任何提示前要遵守的安全规则

先定一些不可协商的规则。它们能让模型聚焦,也能防止你发布只在本地生效的更改。

把工作拆成小步骤。模式更改、数据回填、应用更改和清理步骤都是不同的风险点。把它们捆绑会让故障源难以定位,回滚也更困难。

优先做增加性的改动,而非破坏性的改动。新增列、索引或表通常风险低。重命名或删除对象更容易引发故障。先做安全部分,发布应用,只有在确认旧对象未被使用时再移除旧东西。

让应用在一段时间内能兼容两种数据形态。代码应能同时读取旧列或新列。这样可以避免常见的竞态:部分服务运行新版代码而数据库仍为旧状态(或相反)。

把迁移当作生产级代码对待,而不是一个小脚本。即便你在使用像 Koder.ai 这样的开发平台(Go 后端配 PostgreSQL,前端 React 或 Flutter),数据库是被所有服务共享的。错误代价高昂。

如果你想在每个 SQL 请求顶部放一套紧凑规则,可以使用如下要点:

  • 每次迁移只做一件事:先扩展,再回填,再切换代码,最后清理。
  • 避免长时间加锁:对索引使用 CONCURRENTLY,更新用小批次。
  • 为每个步骤要求回滚计划,包括如何在回填中途停止。
  • 要求验证查询和成功指标(行数、空值率、耗时)。
  • 要求运行手册:如何运行、观察什么、找谁报警。

一个实用的例子:与其重命名一个应用依赖的列,不如新增列,慢速回填,部署先读新列再回退到旧列,确认无误后再移除旧列。

在提示中包含什么以让 Claude Code 不脱离实际

Claude 能从模糊请求生成不错的 SQL,但安全迁移需要上下文。把你的提示当作一个小型设计说明:展示现状,说明不能破坏的点,并定义“安全”对你上线的含义。

先只粘贴那些重要的数据库事实。包括表定义以及相关的索引和约束(主键、唯一约束、外键、CHECK、触发器)。如果涉及关联表,也把相关片段一起给出。小而准确的片段能防止模型猜名或遗漏重要约束。

补充真实规模信息。行数、表大小、写入速率和峰值流量会改变方案。"200M rows and 1k writes/sec" 与 "20k rows and mostly reads" 是完全不同的迁移。还要说明你的 Postgres 版本以及在系统中迁移如何运行(单事务还是多步骤)。

描述应用如何使用这些数据:重要的读取、写入和后台任务。例如:"API 按 email 读"、"worker 更新状态"、或"报表按 created_at 扫描"。这些决定了是否需要 expand/contract、功能开关,以及回填的安全度。

最后,对约束和交付物要具体。一个简单结构通常有效:

  • 当前 schema 片段和目标
  • 规模假设(行数、写入速率、是否有维护窗口)
  • 应用依赖(查询/端点/作业)
  • 硬性约束(无停机、避免长时间锁、避免全表重写)
  • 可交付物:SQL 加上普通语言描述的运行计划、验证和回滚

同时要求 SQL 和运行计划会迫使模型考虑顺序、风险以及上线前需要检查的事项。

用通俗语言解释 expand/contract(何时使用)

expand/contract 迁移模式允许在变更进行中不破坏应用的情况下修改 PostgreSQL。不是一次性切换,而是在一段时间内让数据库同时支持旧形态和新形态。

把它想成:安全地新增(expand),逐步迁移流量与数据,然后再移除旧结构(contract)。这在 AI 辅助工作中特别有用,因为它强制你为中间的混乱做规划。

四个阶段

一个实用流程如下:

  • Expand:新增一个可为空的列或表,必要时新增索引,并以避免阻塞的方式添加约束(例如在合适时用 NOT VALID)。
  • Compatibility:更新应用以同时处理旧字段和新字段,可能采用双写(write to both)或读回退(read new, fall back to old)。
  • Backfill:以小批量把旧数据拷贝到新结构,带检查点并能从失败处恢复。
  • Contract:确认所有流量都走新路径后,收紧规则(例如将列设为 NOT NULL,验证约束),然后删除旧列或旧表。

当用户可能仍在使用旧版应用时就应使用此模式,包括多实例部署、手机应用慢速更新,或任何可能耗时数分钟到数小时的迁移。

一个有用的策略是规划两次发布。发布 1 做 expand + compatibility,以保证即使回填未完成也不会出问题。发布 2 在确认新代码和新数据就位后才做 contract。

一个用于 expand/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,并说明何时运行(离峰或随时)。
  • 强制要求关于锁的诚实描述:"If you're not sure whether a statement takes an ACCESS EXCLUSIVE lock, say so and offer a safer option."(如果不确定某语句是否会获取 ACCESS EXCLUSIVE 锁,请说明并提供更安全的替代方案。)

常见模式操作以及如何提示以获取更安全的 SQL

Practice the safe pattern
Prototype an expand contract change on a real schema before touching production.

即使是小的模式更改也会造成伤害,尤其是在它们导致长时间锁、重写大表或半途失败时。使用 Claude Code 做迁移时,请要求能避免重写并在数据库赶上来的同时保持应用可用的 SQL。

新增列和默认值(避免长锁)

新增可为空列通常安全。在老版本 Postgres 上,新增带非空默认值的列可能会重写整表。

更安全的做法是两步走:先新增 NULL 列且不设默认,分批回填,再为新行设置默认并在数据清理完成后设为 NOT NULL。

如果必须立即强制默认值,请要求对你所用的 Postgres 版本说明锁行为并给出后备方案。

索引、外键、约束、删除

对于大表的索引,请要求使用 CREATE INDEX CONCURRENTLY,以便读写能继续进行。同时提醒它不能在事务块内运行,这意味着你的迁移工具需要支持非事务性步骤。

对于外键,更安全的路径通常是先以 NOT VALID 添加约束,然后在合适时间再验证。这样可以让初始步骤更快,同时仍然对新写入生效。

当要收紧约束(NOT NULL、UNIQUE、CHECK)时,要求 “先清理,再强制”。迁移应检测并修复不合格的行,然后再启用更严格的规则。

在提示中要紧凑地列出检查点:

  • 包含关于锁和预期运行时间的说明。
  • 对大索引使用 CONCURRENTLY,并标明事务限制。
  • 优先 NOT VALID 然后 VALIDATE 来添加外键。
  • 把回填和强制 NOT NULL/UNIQUE 分开。
  • 只有在完整发布周期并确认无人读取旧对象后再删除对象。

针对缓慢、稳健且可恢复的数据回填如何提示

回填是大多数迁移痛点出现的地方,而不是 ALTER TABLE。最安全的提示把回填当成受控作业:可测量、可重启且对生产温和。

从一些容易运行且难以争议的验收检查开始:预期行数、目标空值率,以及一些抽样比对(例如对 20 个随机 ID 比较新旧值)。

然后要求一个分批计划。分批能缩短锁时间并减少意外。好的请求指定:

  • 如何分批(主键范围或基于 created_at 的时间窗口)
  • 目标批次大小(例如 5,000 到 50,000 行)
  • 在热门表上是否在批次间休眠
  • 每个批次为一个明确的事务(不要一个超大事务)

要求幂等性,因为回填会半途失败。SQL 应当能安全地重试而不重复或破坏数据。常见模式是 "只更新 new_col 仍为 NULL 的行" 或使用确定性规则。

还要说明回填运行时应用如何保持正确。如果新写入还在继续,你需要一种桥接方法:在应用中做双写、临时触发器或读回退逻辑,并说明哪种方法在你的场景下安全可行。

最后,把暂停与恢复设计进来。要求进度跟踪与检查点,例如一个小表存储最后处理的 ID,以及报告进度的查询(已更新行数、最后 ID、开始时间)。

示例:你新增 users.full_name,由 first_namelast_name 生成。安全的回填只更新 full_name IS NULL 的行,按 ID 范围分批,每次记录最后处理的 ID,并在切换完成前通过双写保证新注册的数据正确。

如何提示以获得在真实场景可用的回滚计划

Add a rollback safety net
Use snapshots so you can recover quickly if a schema change behaves badly.

回滚计划不仅仅是“写一个 down migration”。它包括两个问题:撤销 schema 更改,以及处理在新版本上线期间发生的数据变化。Schema 回滚通常可行,但数据回滚往往不可行,除非你事先做了规划。

明确说明“回滚”对你的变更意味着什么。如果你要删除列或就地重写值,就要求一个现实的答案,例如:"回滚可以恢复应用兼容性,但原始数据若非快照无法恢复。" 这种诚实的说明能让你更安全地决策。

要求清晰的回滚触发条件以便在事故时没人争执。例如:

  • 错误率或延迟在 10 分钟内超过定义阈值
  • 关键查询计划回退(例如热点表出现 seq scan)
  • 回填作业落后超过 N 小时
  • 数据检查失败(出现不允许的空值、重复、缺失行)
  • 迁移步骤阻塞写入超过 X 秒

要求完整的回滚包,而不仅是 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.

在上线前,采集一个轻量的“安全快照”以便比对:

  • 受影响表的行数(及关键子集)
  • 一小组样本查询及预期结果
  • 触及列的简单聚合(sum, min/max)
  • 可用于抽查的若干 ID 列表

同时明确何时不应回滚。如果你只新增了可空列并且应用在双写,向前修复(热修复代码、暂停回填然后恢复)通常比回滚更安全并且更不容易导致数据漂移。

使用 AI 辅助迁移时常见的错误

AI 能很快写出 SQL,但它看不到你的生产数据库。大多数失败来自于提示模糊,模型自行填充缺失信息。

常见陷阱是跳过去当前 schema。如果你不粘贴表定义、索引和约束,生成的 SQL 可能会针对不存在的列,或忽略一个唯一性规则,使回填成为缓慢且高锁的操作。

另一个错误是把 expand、backfill 和 contract 放在同一次部署里。这会丢失逃生舱。如果回填耗时或半途报错,你就被卡在期待最终状态的应用中。

最常见的问题:

  • 回填不具幂等性且没有进度追踪
  • 在清理和验证数据之前添加 NOT NULL、UNIQUE 或外键
  • 长事务没有锁超时或语句超时
  • 没有验证查询,问题直到用户遇到才发现

一个具体例子:"重命名列并更新应用"。如果生成的计划在单个事务中同时重命名并回填,慢速回填会持有锁并破坏线上流量。更安全的提示会强制要求小批次、明确超时和在移除旧路径前的验证查询。

在发布前在预发布环境需要核实的事项

预发布环境能发现小型开发库中永远不会出现的问题:长时间锁、意外空值、缺失索引、被遗忘的代码路径。

首先,核对迁移后 schema 是否与计划一致:列、类型、默认值、约束与索引。快速浏览不够,一个缺失的索引就能把安全的回填变成慢速灾难。

然后在一个真实数据集上运行迁移。理想情况是使用最近的生产副本并对敏感字段做脱敏。如果不能做到,至少在数据量和热点上模拟生产(大表、宽行、索引密集的表)。记录每一步的耗时,以便在生产中有预期。

一个简短的预发布核对清单:

  • Schema 与计划一致(列、类型、约束、索引)
  • 在真实数据量上记录各步骤耗时
  • 兼容性测试:旧应用对新 schema、新应用对旧 schema(如果计划要求它们应能兼容)
  • 运行验证查询:空值率、行数、外键孤立检查、抽样读取
  • 运行时信号监控:锁、死锁、超时、慢查询

最后,测试真实用户流程,而不仅仅是 SQL。创建、更新和读取受更改影响的记录。如果采用 expand/contract,确认在最终清理前两种 schema 都能正确工作。

一个现实例:在不影响用户的情况下更改列

Own your codebase
Keep full control by exporting the source code once the migration workflow is solid.

假设你有 users.name 列,存储类似 "Ada Lovelace" 的全名。你想要 first_namelast_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_namelast_name,并仍设置 name
  • 现有用户即使只有 name 也能正确显示
  • 回填可停止并重启而不重复工作
  • 回填完成后没有意外的空值残留
  • users 表的基本查询没有明显变慢

发布 2 切换为仅读取新列。只有在那之后才应添加约束(如 SET NOT NULL)并删除 name,最好在之后的单独部署中完成。

回滚要尽量平淡无奇。过渡期间应用继续读取 name,回填可停止。如果需要回滚发布 2,将读取切回 name 并保留新列直到系统稳定。

后续步骤:把你的提示变成可复用的迁移惯例

把每次变更当作一个小型运行手册。目标不是一个完美的提示,而是一套强制要求关键细节的例程:schema、约束、运行计划与回滚。

标准化每个迁移请求必须包含的内容:

  • 当前 schema 与确切变更(表、列、索引)
  • 约束与流量信息(表大小、写入速率、允许的停机时间)
  • 发布顺序(expand、部署应用、回填、contract)
  • 如何观察进度(查询/指标、预期耗时)
  • 回滚步骤(先回滚什么、哪些数据可能残留)

在运行 SQL 前决定好每步的负责人以防止“大家以为别人做”这种情况:开发负责提示和迁移代码,运维负责生产时机与监控,QA 验证预发布行为和边界情况,一人负责最终的 go/no-go。

如果你在聊天中构建应用,先在对话中列出顺序会有帮助。对使用 Koder.ai 的团队来说,Planning Mode 是记录这些顺序的自然位置,快照与回滚可以在上线过程中降低爆炸半径。

发布后尽快安排 contract 的清理工作,趁上下文还新鲜,不要让旧列和临时兼容代码滞留数月。

常见问题

Why do PostgreSQL schema changes break production even when the SQL looks simple?

一个模式更改之所以危险,是因为应用代码、数据库状态和部署时机不再一致

常见失败模式:

  • 旧版应用代码访问了新列或新约束导致崩溃
  • 迁移对繁忙表加了强锁导致请求超时
  • 一次“很小”的更改悄然重写或删除了数据
  • 索引/约束工作比预期耗时更久,导致查询变慢
What’s the safest default way to change a schema without downtime?

使用 expand/contract(扩展/收缩) 方法:

  • Expand(扩展): 以兼容方式添加可为空的新列/表/索引
  • Compatibility(兼容): 部署能同时读写两种结构的应用代码
  • Backfill(回填): 以小批量、带检查点的方式迁移旧数据
  • Contract(收缩): 在完成一次完整发布周期后再收紧约束并移除旧字段

这样可以在回滚或逐步发布期间保持旧版和新版应用的兼容性。

What extra risks do AI-generated migrations introduce?

模型可能生成语法正确但不适合你工作负载的 SQL,这是额外的风险。

AI 特有的典型风险:

  • 猜测表名/列名或遗漏重要约束
  • 提议一次性“大爆发”式迁移,丧失回滚选项
  • 忽视锁行为、事务限制或耗时的索引构建
  • 对回滚轻描淡写(尤其在数据被转换或删除时)

把 AI 的输出当作草稿,要求同时提供运行计划、检查点和回滚步骤。

What should I paste into my prompt so Claude Code doesn’t guess?

把迁移依赖的事实都粘贴进来,避免模型胡乱猜测:

  • 相关的 CREATE TABLE 片段(以及索引、外键、UNIQUE/CHECK 约束、触发器)
  • Postgres 版本及迁移在你系统中如何运行(单事务还是多步)
  • 规模信息:行数、表大小、写入速率、峰值流量
  • 应用如何使用这些数据(关键的读/写/后台任务)
  • 硬性约束(无停机、避免整表重写、锁时长上限)
  • 可交付物:UP SQL + 验证查询 + 回滚计划 + 运行手册

这些信息能防止猜测并强制模型考虑正确的顺序。

Should I combine schema changes and backfills in one migration?

默认规则:分开它们

一个实用划分:

  • 迁移 1:schema 扩展(新增列/表,可能先用 NOT VALID 的约束)
  • 应用部署:兼容代码(读回退或双写)
  • 回填作业:带进度跟踪的小批量更新
  • 迁移 2:收缩(验证约束、设 NOT NULL、删除旧列)

把所有步骤捆绑在一起会让故障更难排查和回滚。

How do I add a new column with a default without causing long locks?

优先采用以下模式:

  1. ADD COLUMN ... NULL(不设默认,快速)
  2. 以批次回填
  3. 为新写入设置默认
  4. 在验证无误后添加 NOT NULL

在某些 Postgres 版本上,添加非空默认值可能会重写整表,如果必须立即添加默认,请要求模型说明锁行为并给出更安全的后备方案。

When should I use CREATE INDEX CONCURRENTLY, and what’s the catch?

建议:

  • 对大表/高并发表使用 CREATE INDEX CONCURRENTLY
  • 说明它不能在事务块内运行(你的迁移工具需支持非事务性步骤)
  • 给出预期运行时间并说明要监控的指标(锁等待、查询延迟)

在验证阶段,包含一个快速检查,确认索引存在且被使用(例如在 staging 中对比 EXPLAIN 计划)。

What’s the safest way to add a foreign key on a large table?

推荐做法:先用 NOT VALID,随后在可控时机验证:

  • 先以 NOT VALID 添加外键约束,使初始步骤更快且不那么侵入
  • 在能监控时单独运行 VALIDATE CONSTRAINT 来验证已有数据

这可以使新写入仍然被约束,但把昂贵的验证延后到你能观察的窗口内。

How do I prompt for a backfill that won’t melt production and can resume?

良好的回填应当是分批、幂等、可重启的。

实用要求:

  • 按主键范围或时间窗口分批
  • 只更新仍需处理的行(例如 WHERE new_col IS NULL
  • 每批使用短事务运行;在热门表上可在批次间休眠
  • 跟踪进度(最后处理的 ID、已更新行数、开始时间)
  • 确保在回填运行期间应用保持正确(双写、触发器或读回退)

这能让回填在真实流量下存活并可恢复。

What does a realistic rollback plan look like for schema changes?

回滚目标:尽快恢复应用兼容性,即便无法完美还原数据。

可执行的回滚计划应包含:

  • DOWN SQL 是否真安全;若不安全则提供运行手册而非盲目回滚 SQL
  • 明确顺序:停止/暂停回填作业,部署回滚的应用代码,随后执行 schema 步骤
  • 清晰的回滚触发条件(错误率、延迟、锁等待、数据检查失败等)
  • 明确什么能回滚(schema)与什么不能(数据)

通常最安全的回滚是切回旧字段的读取逻辑,同时保留新列直到系统稳定。

Related posts