Claude Code を使った PostgreSQL マイグレーションのプロンプト。expand-contract の安全な変更、バックフィル、ロールバック計画、リリース前にステージングで確認すべき項目を解説します。

You are helping me plan a PostgreSQL expand-contract migration.
Context
Goal
Hard safety requirements
Deliverables
Output format
スキーマ変更は アプリのコード、データベースの状態、デプロイのタイミングが一致しなくなったときに 危険になります。\n\nよくある失敗パターン:\n\n- 古いアプリコードが新しいカラムや制約に触れてクラッシュする\n- マイグレーションが繁忙テーブルに強いロックを取り、リクエストがタイムアウトする\n- 「小さな」変更がデータを静かに上書きしたり削除したりする\n- インデックスや制約の作業が予想より長引き、クエリが遅くなる
安全なデフォルトは expand/contract アプローチです:\n\n- Expand: 互換性を保ちながら nullable なカラム/テーブル/インデックスを追加する\n- Compatibility: 両方のスキーマを扱えるようにアプリをデプロイする(読取り/書込みの両対応)\n- Backfill: 小さなバッチでデータを移し、チェックポイントを設ける\n- Contract: フルリリース後に制約を厳しくし、古いフィールドを削除する\n\nこの方法ならリリース中も古い・新しいバージョンの両方が動作し続けます。
モデルは正しい SQL を生成できますが、それが あなたの実際のワークロードに対して安全かどうかは別問題 です。\n\nAI 特有のリスク:\n\n- テーブル名やカラム名を推測してしまう\n- ロールバック手順を軽視し、“一発で”やってしまう案を出す\n- ロック挙動やトランザクション制限、長時間のインデックス作成を無視する\n- データ変換/削除が起きたときの現実的な復旧計画を書かない\n\nAI の出力は草案として扱い、必ず実行計画(ランブック)と検証・ロールバック手順を要求してください。
プロンプトにはマイグレーションで依存する事実だけを入れてください:\n\n- 関連する CREATE TABLE スニペット(インデックス、FK、UNIQUE/CHECK、トリガー含む)\n- Postgres のバージョンとマイグレーションの実行方法(単一トランザクションか複数ステップか)\n- スケール情報:行数、テーブルサイズ、書き込み率、ピークトラフィック\n- アプリのデータ利用方法(重要な読み取り/書き込み/ジョブ)\n- ハード制約(ダウンタイム不可、テーブル全体書き換え禁止、ロック制限など)\n- 求める成果物:UP SQL + 検証クエリ + ロールバック計画 + ランブック\n\nこれでモデルの推測を減らし、正しい順序を引き出せます。
原則として 分ける べきです。\n\n実践的な分割:\n\n- マイグレーション 1: スキーマの expand(新しいカラム/テーブル、場合によっては NOT VALID 制約)\n- アプリデプロイ: 互換性コード(読取フォールバックや dual-write)\n- バックフィル: 進捗追跡付きのバッチ更新\n- マイグレーション 2: contract(制約検証、NOT NULL 設定、古いカラム削除)\n\n一つに詰め込むと障害発生時にロールバックや原因特定が難しくなります。
推奨パターン:\n\n1) ADD COLUMN ... NULL(デフォルトなし、速い)\n2) バッチでバックフィル\n3) 新規行用のデフォルトを設定\n4) 検証後に NOT NULL を追加\n\n一部の古い Postgres バージョンでは非 NULL デフォルトの追加がテーブル全体書き換えを引き起こすため注意が必要です。即時デフォルトが必要な場合はロック挙動の説明とフェールバック案を求めてください。
大きな/ホットなテーブルでは CREATE INDEX CONCURRENTLY を使ってください。注意点:\n\n- CONCURRENTLY はトランザクションブロック内で実行できません(ツールが非トランザクションステップをサポートする必要あり)\n- 期待される実行時間と監視すべき指標(ロック待ち、クエリ遅延)を明示する\n\n検証ではステージングで EXPLAIN 等を比較してインデックスが使われていることを確認してください。
安全な方法はまず NOT VALID で追加し、後で VALIDATE CONSTRAINT を別ステップで走らせることです。\n\n- 最初の追加は軽くて速い(新しい書き込みには制約が適用される)\n- 重い検証は制御できるタイミングで実行する
良いバックフィルは バッチ化、冪等、再開可能 であるべきです。\n\n実践的要件:\n\n- プライマリキー範囲や作成日時などでバッチ化する\n- まだ処理が必要な行だけ更新する(例: WHERE new_col IS NULL)\n- 各バッチは短いトランザクションにする。必要なら各バッチ間で休止を入れる\n- 進捗を追う(最後に処理した ID、更新行数、開始時刻)\n- バックフィル中もアプリが正しく動く方法(dual-write、トリガー、読取りフォールバック)を用意する\n\nこれで本番トラフィックでの耐久性が増します。
現実的なロールバック目標は アプリ互換性を素早く復元すること であり、データを完全に元に戻せるとは限らない点を明確にするべきです。\n\n良いロールバック計画に含めるべき事項:\n\n- DOWN SQL が本当に安全かどうか。安全でなければランブックにする\n- 正確な順序:バックフィル停止→アプリ差し替え→スキーマ操作等\n- ロールバックトリガー(エラー率、レイテンシ、ロック待ち、データ検証失敗など)\n- 何がロールバック可能か(スキーマ)と何が不可能か(データ)の明示\n\n多くの場合、古いフィールドに読み戻す(reads back to old field)方が、新しいカラムを削除して完全復元するより安全です。
UP migration SQL (expand)
Verification queries
Rollback plan (realistic)
Runbook notes
\n\n二つの補助行(実務で役立ちます):\n\n- 書き込みをブロックするステップには `RISK: blocks writes` とラベルを付け、いつ実行するか(オフピークなど)を示させる。\n- ロックについて不確かなら「この文が ACCESS EXCLUSIVE ロックを取るか不明ならそう書いて、より安全な代替を示して」と強制する。\n\n## よくあるスキーマ操作と安全な SQL を引き出すプロンプトの書き方\n\n小さなスキーマ変更でも、長時間のロックや大規模なテーブル書き換え、途中失敗で害が出ます。Claude Code に依頼するときは、書き換えを避けつつアプリが追いつく間に安全に動く SQL を求めてください。\n\n### デフォルト付きカラムの追加(長時間ロックを避ける)\n\nnullable カラムの追加は通常安全です。非 NULL のデフォルトを即座に設定すると、古い PostgreSQL ではテーブル全体を書き換えてしまうことがあり危険です。\n\n安全な手順は 2 ステップ:まず NULL 許容でデフォルトなしのカラムを追加し、バッチでバックフィルし、その後新規行のデフォルトを設定して NOT NULL を追加します。\n\n即時デフォルトが絶対に必要なら、Postgres のバージョンごとのロック挙動の説明と、想定より長くかかった場合の代替案を要求してください。\n\n### インデックス、FK、制約、削除\n\n大きなテーブルへのインデックスは `CREATE INDEX CONCURRENTLY` を要求してください。これにより読み書きは継続しますが、トランザクション内では実行できない点に注意してください。\n\n外部キーは通常 `NOT VALID` として追加し、後で `VALIDATE` する方が安全です。これで初期変更は速くなり、新しい書き込みには制約が効きます。\n\n制約を厳しくする(NOT NULL、UNIQUE、CHECK)ときは「まずクリーンアップ、次に適用」の流れにしてください。マイグレーションは不整合行を検出して修正し、それから厳格化するべきです。\n\n短いチェックリストをプロンプトに貼るなら:\n\n- ロックと想定実行時間について注記する\n- 大きなインデックスは CONCURRENTLY を使い、トランザクション制約を明示する\n- 新しい FK は NOT VALID で追加し後で VALIDATE する\n- バックフィルと NOT NULL/UNIQUE の適用は分離する\n- オブジェクトはフルリリースサイクル後かつ参照されていないことを確認してから削除する\n\n## ゆっくり・安全に再開可能なデータバックフィルのプロンプト\n\nバックフィルが最も痛く出ることが多く、`ALTER TABLE` よりバックフィル自体の設計が重要です。安全なプロンプトはバックフィルをジョブとして扱います:測定可能で再開可能、かつ本番に優しい。\n\nまず受け入れ基準を決めてください:期待行数、目標 NULL 率、20 件程度のランダム ID 比較などのスポットチェック。\n\n次にバッチ戦略を要求します。バッチはロックを短くし、驚きを減らします。良いリクエストは次を具体的に示します:\n\n- バッチ方法(PK 範囲や created_at の時間窓)\n- 目標バッチサイズ(例:5,000〜50,000 行)\n- ホットなテーブルではバッチ間にスリープを入れるかどうか\n- 各バッチは短いトランザクションにする(巨大な一回のトランザクションは避ける)\n\nバックフィルは冪等であることを要求してください。中断しても再実行して重複や破損を招かない設計にします。典型パターンは「new_col IS NULL の行だけ更新する」や、入力から常に同じ出力が得られる決定的な処理です。\n\nまた、バックフィル中にアプリが正しく動く方法を明確にしてください。新規書き込みが続くなら、アプリ側での dual-write、トリガー、または read-fallback(new を優先してなければ old)を使う必要があります。どれが安全にデプロイできるのかを指定してください。\n\n最後に一時停止と再開を設計に組み込んでください。進捗を示す小さなテーブル(最後に処理した ID を保存する等)と、進捗を報告するクエリ(更新済み行数、最後の ID、開始時刻)を要求します。\n\n例:`users.full_name` を `first_name` と `last_name` から作る場合。安全なバックフィルは `full_name IS NULL` の行だけを ID 範囲で 1,000 行ずつ更新し、最後に処理した ID を記録し、新規登録は dual-write で整合性を保つ、というような設計です。\n\n## 実際に役立つロールバック計画のプロンプト\n\nロールバック計画は単に「下向きのマイグレーションを書く」だけではありません。問題は二つ:スキーマ変更を元に戻すことと、新しいバージョンが動いている間に変わったデータをどう扱うか、です。スキーマのロールバックは可能なことが多いですが、データのロールバックは事前に計画していなければ不可能なことが多いです。\n\nロールバックで何を意味するかを明確にしてください。カラムを削除したり値をインプレースで書き換えたりするなら、「ロールバックでアプリ互換性は戻せるが、元のデータはスナップショットがなければ復元できない」等の現実的な答えを要求してください。誠実な説明が安全性を高めます。\n\n決断を迷わせないためにロールバックトリガーを明確にしてください。例:\n\n- エラー率やレイテンシが 10 分間所定の閾値を超えた\n- 重要クエリのプランが劣化(ホットテーブルでのシーケンシャルスキャン等)\n- バックフィルが N 時間以上遅延している\n- データ検査が失敗(NULL や重複、欠落行)\n- マイグレーションが X 秒以上書き込みをブロックしている\n\n下向き SQL(安全なら)に加え、アプリ設定/コードの切り替え方法、バックグラウンドジョブの停止方法なども含めた完全なロールバックパッケージを要求してください。\n\nこのようなプロンプトがあれば十分です:\n\n
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.
\n\n出荷前に軽い「安全スナップショット」を取っておくと比較が簡単になります:\n\n- 影響を受けるテーブルの行数(および重要なサブセット)\n- サンプルクエリと期待結果の小セット\n- 変更カラムの簡単な集計(sum、min/max)\n- 事前/事後でスポットチェックするための ID リスト\n\nまた、「いつロールバックすべきでないか」も明記してください。例えば、nullable カラムを追加してアプリが dual-write している場合は、ロールバックより前進で修正(ホットフィックス、バックフィル一時停止、再開)の方が安全なことが多いです。\n\n## AI 支援マイグレーションで気をつける一般的なミス\n\nAI は高速に SQL を書けますが、本番データベースを見ているわけではありません。多くの失敗はプロンプトが曖昧でモデルが穴埋めしてしまうことに起因します。\n\nよくある落とし穴は現在のスキーマを省略することです。テーブル定義やインデックス、制約を貼らないと、生成された SQL が存在しないカラムを使ったり、一意性ルールを見落としてバックフィルがロック地獄になることがあります。\n\nまた expand、backfill、contract を一度に出荷するのも危険です。これだと逃げ道がなくなります。バックフィルが長引いたり途中で失敗した場合に、最終状態を期待するアプリが先行してしまうと元に戻せません。\n\nよくある問題点:\n\n- 冪等でなく進捗追跡がないバックフィル\n- データをクリーンにしてから NOT NULL / UNIQUE / FK を追加していない\n- ロックタイムアウトやステートメントタイムアウトがない長時間トランザクション\n- 検証クエリがなく問題がユーザに見つかるまで潜む\n\n具体例:「カラム名をリネームしてアプリを更新する」生成されたプランがリネームとバックフィルを単一トランザクションでやると、長いバックフィルがロックを保持してライブトラフィックを壊します。安全なプロンプトは小さなバッチ、明示的なタイムアウト、最終パス削除前の検証クエリを強制します。\n\n## 出荷前にステージングで確認すること\n\nステージングは小さな開発 DB では出ない問題を見つける場所です:長時間ロック、予期しない NULL、欠落インデックス、見落とされたコードパス。\n\nまず、マイグレーション後にスキーマが計画通りか確認してください:カラム、型、デフォルト、制約、インデックス。表面的な確認では十分ではありません。1 つの欠落インデックスが安全なバックフィルを遅くすることがあります。\n\n次に実データに近いデータセットでマイグレーションを実行してください。理想は本番の最近コピー(機微なフィールドはマスク)です。できない場合でも、少なくとも本番のボリュームやホットスポット(大きなテーブル、広い行、インデックス多用)を模すべきです。各ステップのタイミングを記録して本番の見積もりに役立てます。\n\n短いステージングチェックリスト:\n\n- スキーマが計画通り(カラム、型、制約、インデックス)\n- 実データボリュームでの各ステップの実行時間を記録\n- 互換性のテスト:古いアプリ+新スキーマ、新アプリ+旧スキーマ(計画どおりなら両方動くか)\n- 検証クエリ実行:NULL 率、行数、FK の孤児チェック、サンプル読み取り\n- 実行中にみる運用指標:ロック、デッドロック、タイムアウト、遅いクエリ\n\n最後に、SQL だけでなく実際のユーザフロー(作成、更新、読み取り)をテストしてください。expand/contract が計画なら、最終クリーンアップまで両方のスキーマが動作することを確認します。\n\n## 現実的な例:ユーザを壊さずにカラムを分割する\n\n`users.name` に "Ada Lovelace" のようなフルネームが入っているとします。`first_name` と `last_name` に分けたいが、サインアップやプロフィール、管理画面をロールアウト中に壊せません。\n\nまず expand ステップで安全に始めます:nullable な新カラムを追加し、古いカラムは残し、長時間のロックを避けます。\n\nsql
ALTER TABLE users ADD COLUMN first_name text;
ALTER TABLE users ADD COLUMN last_name text;
```\n\n次にアプリの挙動を両スキーマに対応するように更新します。リリース1 では新しいカラムがあればそれを読み、なければ name にフォールバックし、新規データは両方に書くようにします。\n\nその後バックフィルを行います。小さなチャンクずつ更新し、進捗を記録して安全に一時停止・再開できるジョブを走らせます。例えば first_name IS NULL の行を ID 昇順で 1,000 行ずつ更新し、更新数をログに残すなどです。\n\n制約を厳しくする前にステージングで検証してください:\n\n- 新規登録が first_name と last_name を埋め、同時に name も設定する\n- 既存ユーザは name のみでも正しく表示される\n- バックフィルは停止・再開しても重複作業をしない\n- 完了後に予期せぬ NULL が残らない\n- users に対する基本クエリが著しく遅くならない\n\nリリース2 で読み取りを新カラムオンリーに切り替えます。制約(SET NOT NULL)や name の削除はさらに後の別デプロイで行うのが望ましいです。\n\nロールバックは地味にしておきましょう。移行中はアプリが name を読み続け、バックフィルは中断可能にしておきます。もしリリース2 を巻き戻す必要があれば、読み取りを name に戻し、新しいカラムは残したまま安定するまで待ちます。\n\n## 次のステップ:プロンプトを繰り返し可能なマイグレーションルーチンにする\n\n各変更を小さなランブックとして扱ってください。目的は完璧なプロンプトではなく、スキーマ・制約・実行計画・ロールバックの正しい詳細を強制するルーチンを作ることです。\n\nすべてのマイグレーション要求で標準化する項目を決めてください:\n\n- 現在のスキーマと正確な変更点(テーブル、カラム、インデックス)\n- 制約とトラフィック事実(テーブルサイズ、書き込み率、許容ダウンタイム)\n- リリース順序(expand、アプリデプロイ、backfill、contract)\n- 進捗の可視化方法(クエリ/メトリクス、期待実行時間)\n- ロールバック手順(何を最初に戻すか、どのデータが残るか)\n\n誰が各ステップを担当するかを事前に決めておくと「誰かがやるだろう」問題を防げます:開発がプロンプトとマイグレーションコード、運用が本番タイミングと監視、QA がステージング検証、そして最終の go/no-go を決める一人を置く、など。\n\nチャットでアプリを作る場合は、SQL を生成する前に順序を書き出しておくと助かります。Koder.ai を使うチームなら Planning Mode にシーケンスを書き残し、スナップショットとロールバック計画を用意しておくことでロールアウト時の被害を小さくできます。\n\n出荷後はコンテキストが新鮮なうちにすぐ contract のクリーンアップをスケジュールし、古いカラムや互換性コードが数ヶ月残らないようにしてください。