Claude CodeでDBマイグレーションを安全に進める実践ガイド
Claude Codeで本番DB移行を安全に進める手順。expand/contract、Prisma、CI、バックフィルまで解説。
本番データベースのマイグレーションは、Claude Codeに「変更して」と頼むだけでは危険です。アプリのデプロイ、DBロック、バックフィル、ロールバック、監査ログが同時に絡むため、正しい順番を設計してからエージェントに作業させる必要があります。
この記事では、Claude Codeを「SQLを書く係」ではなく「移行計画をレビューし、危険な変更を分解する相棒」として使う方法をまとめます。対象はPostgreSQLとPrisma Migrateを使うSaaSチームです。SQLだけで移行しているチームにも使えるように、Prismaの例と生SQLの例を並べます。
公式情報は必ず手元で確認してください。Claude Codeの基本はAnthropic公式ドキュメント、PostgreSQLのロックは明示的ロックとALTER TABLE、Prismaの本番運用はPrisma Migrateのdevelopment and productionとCLI reference、CIはGitHub Actions workflow syntaxを参照します。
全体像
本番DB移行の基本は「expand/contract」です。expandは、古いアプリと新しいアプリの両方が動けるようにDBを広げる段階です。contractは、全ユーザーが新しい経路に移った後で古い列や制約を片付ける段階です。日本語で言えば「先に受け皿を広げ、動作を切り替え、最後に掃除する」考え方です。
flowchart LR
A["Backup and review"]
B["Expand: add nullable column or new table"]
C["Deploy code with dual write or feature flag"]
D["Backfill data in small batches"]
E["Validate in staging and production metrics"]
F["Contract: add NOT NULL, remove old path"]
A --> B --> C --> D --> E --> F
Claude Codeに最初から「全部やって」と頼むと、列追加、データ更新、NOT NULL化、古い列の削除を1つのマイグレーションにまとめがちです。小さなテーブルなら通ることもありますが、数百万行のusersやordersで同じことをすると、長時間ロック、書き込み停止、タイムアウト、復旧不能なデータ消失が起きます。
用語も揃えておきます。ロックは、DBが同じ表を同時に壊さないための待ち合わせです。バックフィルは、既存行に新しい列の値を少しずつ埋める作業です。shadow databaseは、Prismaが開発時にマイグレーション履歴を再生して差分を検査する一時DBです。本番で何かを自動修復する魔法ではありません。
Claude Codeへのレビュー依頼
最初のプロンプトは実装依頼ではなくレビュー依頼にします。既存のschema.prisma、マイグレーション履歴、テーブル規模、デプロイ方法を読ませ、危険箇所を列挙させます。
Review this database migration plan before editing files.
Context:
- Production database: PostgreSQL
- ORM: Prisma Migrate
- Hot tables: users has about 8 million rows, orders has about 25 million rows
- Deploy style: blue/green app deploy, database migration runs in CI/CD
- Requirement: split users.name into users.full_name and users.display_name
Check:
1. Can old and new app versions run at the same time?
2. Which SQL statements may take strong locks or scan the whole table?
3. Which steps must be expand, backfill, validate, and contract?
4. What backup or point-in-time recovery check is needed before deploy?
5. What can be rolled back by app deploy, and what can only be rolled forward?
Return a migration plan first. Do not edit files yet.
ここで重要なのは「編集するな」と明示することです。Claude Codeは速いので、曖昧な指示だとマイグレーションファイルまで作り切ります。しかしDB移行では、実装前のレビューが本体です。計画が怪しければ、次のように追加で詰めます。
Rewrite the plan so that no step drops a column, rewrites a large table, or sets NOT NULL before the backfill is verified. Include a staging rehearsal and a production abort condition.
この2段階にすると、Claude Codeの価値が出ます。人間は「このALTERは本当に今やるのか」を判断し、Claude Codeは「関連ファイル、CI、SQL、検証コマンドを抜け漏れなく並べる」役になります。
ExpandのSQL例
例として、users.nameを将来廃止し、full_nameとdisplay_nameへ移すケースを考えます。expand段階では、古い列を残したまま新しい列をnullableで追加します。NOT NULLやDROP COLUMNはまだ行いません。
-- 20260602090000_expand_users_names.sql
-- Keep this migration small. Do not backfill and do not drop users.name here.
ALTER TABLE users
ADD COLUMN full_name text,
ADD COLUMN display_name text;
-- Run outside a transaction in PostgreSQL migration tools that support it.
-- CREATE INDEX CONCURRENTLY cannot run inside a transaction block.
CREATE INDEX CONCURRENTLY IF NOT EXISTS users_display_name_idx
ON users (display_name);
PostgreSQLのALTER TABLEは、サブコマンドによって必要なロックが変わります。公式ドキュメントでは、明示がない場合は強いACCESS EXCLUSIVEロックが取られると説明されています。だからClaude Codeには「ロックレベルを推測で書かず、公式ドキュメントに照らして保守的にレビューして」と頼みます。
Prismaを使っている場合も、生成されたSQLをそのまま本番投入しないでください。--create-onlyでマイグレーションを作り、SQLをレビューしてから適用します。
npx prisma migrate dev --name expand-users-names --create-only
npx prisma validate
Prismaの公式ガイドでも、カスタムSQLが必要な場合は--create-onlyで作成して編集する流れが紹介されています。本番やテスト環境に適用するコマンドはnpx prisma migrate deployです。ただしPrismaの説明どおり、migrate deployはdrift検出をせず、shadow databaseにも依存しません。つまり「CIでdeployが通った」だけでは、本番DBが期待通りかまでは保証できません。
アプリ側の切り替え
expand後のアプリは、古い列と新しい列の両方を扱える必要があります。安全な順番は、読み取りは古い列を優先、書き込みは新旧両方、バックフィル完了後に読み取りを新しい列へ切り替え、最後に古い列を消す、です。
// src/domain/userName.ts
type UserNameRow = {
name: string | null;
fullName: string | null;
displayName: string | null;
};
export function readDisplayName(user: UserNameRow): string {
return user.displayName ?? user.fullName ?? user.name ?? "Unknown user";
}
export function buildNameUpdate(input: { name: string }) {
const normalized = input.name.trim().replace(/\s+/g, " ");
return {
name: normalized,
fullName: normalized,
displayName: normalized.length > 40 ? `${normalized.slice(0, 39)}...` : normalized,
};
}
ここでフィーチャーフラグを使うと、DB移行とユーザー体験の切り替えを分けられます。フラグがOFFなら古い読み取り経路、ONなら新しい読み取り経路にします。DBの変更は簡単に巻き戻せませんが、アプリの読み取り経路はデプロイやフラグで戻せます。フィーチャーフラグの設計はClaude Codeでフィーチャーフラグを実装する方法も合わせて確認してください。
バックフィルは小さく実行する
バックフィルを1つの巨大なUPDATEで実行すると、ロック時間、WAL量、レプリケーション遅延、オートバキューム負荷が増えます。Claude Codeには、アプリのバッチスクリプトとして小さく更新する案を出させる方が安全です。
// scripts/backfill-user-names.mjs
import pg from "pg";
const { Client } = pg;
const batchSize = Number(process.env.BATCH_SIZE ?? 1000);
const sleepMs = Number(process.env.SLEEP_MS ?? 200);
const client = new Client({ connectionString: process.env.DATABASE_URL });
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
await client.connect();
try {
let total = 0;
while (true) {
const result = await client.query(
`
WITH target AS (
SELECT id, name
FROM users
WHERE full_name IS NULL
AND name IS NOT NULL
ORDER BY id
LIMIT $1
FOR UPDATE SKIP LOCKED
)
UPDATE users AS u
SET
full_name = target.name,
display_name = CASE
WHEN length(target.name) > 40 THEN substring(target.name from 1 for 39) || '...'
ELSE target.name
END
FROM target
WHERE u.id = target.id
RETURNING u.id
`,
[batchSize],
);
total += result.rowCount;
console.log(`updated=${result.rowCount} total=${total}`);
if (result.rowCount === 0) break;
await sleep(sleepMs);
}
} finally {
await client.end();
}
このコードはコピーして使える形にしていますが、実運用ではusers.idの型、インデックス、レプリカ遅延、実行時間帯、監視アラートに合わせて調整します。Claude Codeには「このスクリプトが並列実行された場合」「途中で落ちた場合」「同じ行を再更新した場合」をレビューさせます。
ユースケースは少なくとも3つあります。1つ目はユーザー名、住所、電話番号の正規化です。2つ目は注文合計、請求書残高、検索用カラムなど、既存データから計算できる列の追加です。3つ目は外部キーやインデックスを後から追加するケースです。いずれも、列追加、アプリ切り替え、バックフィル、検証、制約追加を分けると事故率が下がります。
CIとステージング
GitHub Actionsでは、PRごとに一時PostgreSQLを立て、マイグレーションを最初から適用します。WorkflowファイルはGitHub公式ドキュメントの通り.github/workflows配下のYAMLとして管理します。
name: migration-check
on:
pull_request:
paths:
- "prisma/**"
- "scripts/backfill-*.mjs"
- ".github/workflows/migration-check.yml"
jobs:
prisma-migrations:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app
ports:
- "5432:5432"
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/app?schema=public
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
- run: npm ci
- run: npx prisma validate
- run: npx prisma migrate deploy
- run: npx prisma migrate status
- name: Detect schema drift after migrations
run: |
npx prisma migrate diff \
--exit-code \
--from-config-datasource \
--to-schema=prisma/schema.prisma
この例はPrisma ORM v7以降の--from-config-datasource構文を使っています。古い記事や古いCI設定にある--from-urlや--shadow-database-urlをそのままコピーしないでください。Prisma CLI referenceでは、v7でこれらの一部オプションが削除されたことが明記されています。
ステージングでは、CIより現実に近い確認をします。本番に近い行数、同じインデックス、同じタイムアウト、同じマイグレーションランナーを使います。Claude Codeには、EXPLAIN、ロック待ち、レプリケーション遅延、バックフィル時間、アプリログの確認項目をチェックリスト化させます。
Contractとロールバックの限界
バックフィルが完了し、アプリが新しい列だけで動くことを確認してからcontractを行います。NOT NULLをいきなり付けるのではなく、先に検証用制約を使ってデータの穴を確認します。
-- 20260602120000_contract_users_names.sql
-- Run only after the new application version has been stable in production.
ALTER TABLE users
ADD CONSTRAINT users_full_name_present
CHECK (full_name IS NOT NULL) NOT VALID;
ALTER TABLE users
VALIDATE CONSTRAINT users_full_name_present;
ALTER TABLE users
ALTER COLUMN full_name SET NOT NULL;
ALTER TABLE users
DROP CONSTRAINT users_full_name_present;
-- Drop old columns in a later deploy, not in the same deploy that changes reads.
-- ALTER TABLE users DROP COLUMN name;
ロールバックで一番危ない誤解は「down migrationがあれば戻せる」です。列をdropした後のデータ、型変換で丸めた値、上書きした履歴は、SQLを逆向きにしても元に戻りません。戻せるのは多くの場合、アプリのバージョン、フィーチャーフラグ、まだ消していない読み取り経路です。DB側はバックアップ、ポイントインタイムリカバリ、または前向き修正で考えます。
Prismaのmigrate resolve --rolled-backも、成功したマイグレーションを魔法のように戻すコマンドではありません。公式CLI referenceの説明どおり、失敗したマイグレーションの履歴状態を解決するためのコマンドです。Claude Codeにロールバック案を出させる時は「データ復元が必要な手順と、アプリだけ戻せる手順を分けて」と指定します。
よくある失敗
1つ目は、renameをdrop and addとして生成させることです。PrismaやORMは意図を完璧には読めません。nameをfullNameに変えたつもりでも、生成SQLが列削除と列追加ならデータは失われます。
2つ目は、ALTER TABLEと巨大なUPDATEを同じマイグレーションに入れることです。失敗した時に原因がロックなのか、データ内容なのか、タイムアウトなのか切り分けにくくなります。
3つ目は、shadow databaseを過信することです。shadow databaseは開発時の履歴再生やdrift検出に役立ちますが、本番のロック時間、実データ分布、レプリケーション遅延までは見ません。
4つ目は、バックアップ確認を「誰かがやっているはず」で済ませることです。移行前には、最新バックアップの時刻、復元先、復元手順、責任者、想定復旧時間を確認します。これはClaude Codeではなくチームの運用責任です。
Claude Codeをチームに組み込む
チームで使うなら、CLAUDE.mdにDB移行ルールを書いておきます。たとえば「本番DBではdrop columnを同一PRに入れない」「大テーブルのbackfillはアプリスクリプトで小分け」「Prisma migrationは--create-onlyでSQLレビュー」「公式ドキュメントURLをレビューコメントに含める」といったルールです。書き方はCLAUDE.mdベストプラクティスで詳しく解説しています。
収益に直結するSaaSでは、DB移行の失敗は単なる技術負債ではなく売上停止です。ClaudeCodeLabの教材・テンプレートではレビュー用プロンプトやチェックリストを増やしており、チーム導入の流れはClaude Code研修・導入相談で整理できます。
この記事で紹介した内容を実際に試した結果、最も効果が大きかったのは「SQLを賢くすること」ではなく「一度にやることを減らすこと」でした。expandだけのPR、バックフィルだけのジョブ、contractだけの後続PRに分けると、Claude Codeの出力も人間のレビューも具体的になります。Masaの現場メモとしても、DB移行ではエージェントの速度より、停止条件と復旧手順を先に書かせる方が本番事故を減らせます。
まとめ
Claude Codeは本番DB移行を自動で安全にしてくれる道具ではありません。しかし、expand/contract、CI、ステージング、バックフィル、feature flag、バックアップ確認を明示すれば、レビュー漏れを減らす強力な相棒になります。
まずは次の1件のマイグレーションで、Claude Codeに「実装」ではなく「危険な順番を指摘するレビュー」を頼んでください。そこからSQL、Prisma、GitHub Actions、ロールバック計画を小さく積み上げるのが、 production team にとって一番堅い使い方です。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。