データベースブランチング:PostgresにGitワークフローを適用する方法(2026年完全ガイド)
これ、経験ありますよね。チームが1日15個のPRを出す。各PRにVercelやNetlifyで綺麗なプレビューデプロイが作られる。フロントエンドのコードは隔離されていて、APIルートも動く。そこで誰かがマイグレーションのテストをして、みんなが共有してるステージングDBを吹き飛ばす。他のプレビュー環境が全部同時に壊れる。Slackが炎上する。
問題はデプロイパイプラインじゃないんです。コードにはgit branchがあるのに、データにはない。アプリケーションは何年もブランチを切って開発してきたのに、データベースはずっとモノリスのまま。すべてのデベロッパー、すべてのプレビュー、すべてのCIランが1つのステージングインスタンスを奪い合う構造なんですよね。
データベースブランチングがこの問題を根本から解決する技術です。すべてのPR、すべてのCIラン、すべてのデベロッパーに、本番スキーマとデータを持った隔離されたDBインスタンスを提供します。1秒以内に起動し、コストはほぼゼロ。共有ステージングは終わり。「マイグレーションが終わるまで待って」も終わり。壊れたプレビュー環境も終わり。
このガイドでは、2026年のデータベースブランチングを徹底解剖します。基盤技術であるCopy-on-Write(CoW)の動作原理、各プロバイダーの提供内容、そして最も重要なこと — すべてのPRが自動的に自分だけのDBブランチを持つように、CI/CDパイプラインに組み込む方法まで。
従来のデータベース環境が壊れている理由
従来のDB環境の標準的な構成はこうです:
本番DB → ステージングDB → ローカルDB (docker-compose)
↑
全員が共有するポイント
この構造が3つの根本的な問題を生みます:
1. スキーマドリフト
ステージングDBが本番と6ヶ月前に乖離した。誰かが手動でマイグレーションを走らせ、誰かがハードコードされたIDでテストデータを入れた。今やステージングには本番にない14個のカラムがある。「ステージングでは動いた」マイグレーションが本番で失敗する — これが原因。
2. 競合(Contention)
デベロッパーAはNOT NULLカラムをデフォルト値つきで追加するマイグレーションをテスト中。デベロッパーBはテーブルをドロップするマイグレーションをテスト中。2人とも同じステージングDBを見ている。どちらかがひどい午後を過ごすことになる。
3. データの不一致
ローカルのdocker-composeDBにはシードデータが50行。本番のusersテーブルには200以上のエッジケースを含む470万行がある。ローカルで2msのクエリが本番では45秒かかる。実データのボリュームではクエリプランナーがまったく違う実行パスを選ぶから。
データベースブランチングは、この3つをまとめて解消します。各環境が本番からフォークした自分だけのDBを持つ。リアルデータ、リアルスケールで。
データベースブランチングの仕組み
即座にDBブランチを作れる秘密はCopy-on-Write(CoW)ストレージです。このメカニズムを理解すれば、安心して使えます。
Copy-on-Write:ストレージレイヤーでの動作
従来のDBコピーはデータを丸ごと複製します。100GBの本番DBなら80GBのコピーができる。ブランチで実際に触るデータは1%未満なのに、遅くて、高くて、無駄なんですよね。
Copy-on-Writeはこのモデルを反転させます:
本番ブランチ (100 GB)
├── データページ: [A] [B] [C] [D] [E] ... [N]
│
├── PRブランチ #1 (オーバーヘッド: ~50 KB)
│ └── 本番とすべてのページを共有
│ └── 変更されたページ: [C'] ← このページだけコピー
│
└── PRブランチ #2 (オーバーヘッド: ~50 KB)
└── 本番とすべてのページを共有
└── 変更されたページ: [A'] [D'] ← 変更されたページだけ
ブランチが作られる時、データはコピーされません。ブランチは親と同じストレージページを指すポインターにすぎない。データが実際に変更された時だけ、該当ページのコピーが作られます。だから:
- ブランチ作成は即座(DBサイズに関係なく通常1秒以内)
- ストレージコストは変更量に比例、DBサイズには比例しない
- 500GBのDBブランチ作成コストが5MBと同じ
Write Amplificationのトレードオフ
CoWもタダじゃないです。共有ページへの最初の書き込みでページコピーが発生し、その分だけレイテンシが乗ります。とはいえ、テストやプレビュー用途ではほぼ気にならないレベルです。書き込みが多い長期ブランチだけ注意すればOK。
共有ページへの最初の書き込み:
1. 共有ストレージから元のページを読み取り
2. ブランチローカルストレージにページコピー
3. コピーされたページに書き込みを適用
オーバーヘッド: ページあたり最初の書き込みで ~2-5ms
同じページへの以降の書き込み:
1. ブランチローカルページに直接書き込み
オーバーヘッド: 0(通常のDBと同じ)
PRで作られてマージ時に削除される一時的なブランチがCoWに最適な理由がこれです。削除前に変更するデータがほとんどないから。
プロバイダー比較:2026年に誰が何を提供しているか
いくつかのプロバイダーがDBブランチングを提供しています。それぞれトレードオフが異なります:
Neon (PostgreSQL)
Neonは最も成熟したDBブランチングソリューション。ブランチングをコアプリミティブとしてゼロから設計した唯一のソリューションでもあります。
アーキテクチャ: Neonはコンピュート(Postgres)とストレージ(カスタム分散ページサーバー)を分離。ブランチングがストレージレイヤーで行われるため、即座かつゼロコスト。
# Neon CLIでブランチ作成 neonctl branches create \ --project-id my-project \ --name pr-${PR_NUMBER} \ --parent main # 結果: # Branch "pr-142" created in 0.8s # Connection string: postgres://user:[email protected]/mydb
主な機能:
- 任意の時点からの即座ブランチング(PITRをブランチとして活用)
- Scale-to-zeroコンピュート(アイドルブランチのコスト$0)
- ブランチ間スキーマ比較(組み込みマイグレーション可視化)
- Vercelネイティブ統合(プレビューデプロイごとに自動ブランチ)
- ブランチリセット(再作成なしで親と再同期)
制限事項:
- PostgreSQL専用
- Scale-to-zeroからの復帰時にコールドスタート~500ms
- 書き込みの多いブランチではストレージベースの料金が想定以上になる可能性
PlanetScale (MySQL + PostgreSQL)
PlanetScaleは「データベースのためのGitHub」というコンセプトでdeploy requestワークフローを切り開いたサービスです。元々VitessベースのMySQL専用でしたが、今はマネージドPostgreSQLも加わりました。
アーキテクチャ: Vitess側はスキーマレベルの分離を採用。新しいPostgres側はFK、トリガー、ストアドプロシージャまで全部使える本格的なPostgresです。
# PlanetScaleブランチ作成 (Vitess/MySQL) pscale branch create my-database pr-142 # Deploy request(スキーマ変更のためのPRのようなもの) pscale deploy-request create my-database pr-142 \ --into main
主な機能:
- Deploy requests: スキーマ変更に対するPRライクなレビュープロセス (Vitess)
- 本番での非ブロッキングスキーママイグレーション (Vitess)
- スキーマロールバック(デプロイ済みマイグレーションの取り消し)
- PostgresオファリングでFK、トリガー、拡張を完全サポート
制限事項:
- Vitessブランチはスキーマを共有するがデータはデフォルトで空
- Hobbyティア廃止 — 最低コスト$5/月(Postgres)またはリソースベース(Vitess)
- ポイントインタイムブランチ作成非対応
- Postgresブランチのスキーマ変更は手動適用(deploy requestはまだ非対応)
Supabase (PostgreSQL)
Supabaseのブランチングは**2026年3月にGA(正式リリース)**を達成しました。Gitベースのマイグレーションワークフローと緊密に統合されています。
アーキテクチャ: 各ブランチは独自の隔離されたPostgresインスタンスとEdge Functionsを取得。ただしAuthとStorageはメインプロジェクトに紐付いたままで、ブランチごとに独立複製はされません。
# Supabaseブランチング(GitHub統合経由) # GitHubリポジトリに接続するとPRごとに自動プレビューブランチ作成 supabase branches create pr-142 \ --project-ref my-project \ --region us-east-1
主な機能:
- Edge Functionsサポート付きDBブランチング
supabase/migrations/でGitベースのマイグレーション追跡- プレビューブランチが本番Postgresバージョンと一致
- Supabase Studioでのビジュアルスキーマ差分
- PRクローズ時のブランチ自動クリーンアップ
制限事項:
- ブランチは空データで開始(
seed.sqlでシーディング、CoWクローンなし) - AuthとStorageはメインプロジェクトと共有(ブランチごとに独立複製されない)
- ブランチ作成に2-4分(Neonのサブ秒に対してフルインスタンス起動)
- ブランチは
mainブランチにのみマージ可能
Turso (libSQL/SQLite)
Tursoは異なるモデルを適用しています:エッジでの組み込みDBブランチング。
アーキテクチャ: libSQL(SQLiteのフォーク)ベース。各ブランチはエッジで実行可能な軽量レプリカ。
# Tursoブランチ作成 turso db create pr-142 --from-db production # ブランチをアプリケーションプロセスに直接埋め込み可能 # 外部DB接続不要
主な機能:
- 100ms未満のブランチ作成
- 組み込みモード(DBがアプリプロセス内で実行)
- ビルトインマルチリージョンレプリケーション
- 極めて低コスト(Hobbyティア無料、9GBストレージ)
制限事項:
- SQLite互換レイヤー(PostgreSQL/MySQLの完全な機能セットではない)
- 高同時書き込みワークロードに不向き
- エコシステムと統合が少なめ
比較マトリックス
| 機能 | Neon | PlanetScale | Supabase | Turso |
|---|---|---|---|---|
| エンジン | PostgreSQL | MySQL (Vitess) + PostgreSQL | PostgreSQL | libSQL (SQLite) |
| ブランチ作成 | < 1秒 | ~5秒 | 2-4分 | < 100ms |
| データクローン | ✅ Copy-on-Write | ❌ スキーマのみ (Vitess) | ❌ 空 (seed.sql) | ✅ 全コピー |
| Scale-to-Zero | ✅ | ❌ | ❌ | ✅ |
| ポイントインタイム | ✅ | ❌ | ❌ | ❌ |
| Auth/Storage | ❌ DBのみ | ❌ DBのみ | ⚠️ 共有(独立複製なし) | ❌ DBのみ |
| Vercel統合 | ✅ ネイティブ | ✅ ネイティブ | ✅ ネイティブ | ✅ コミュニティ |
| 最低コスト | 無料ティア | $5/月 (Postgres) | 無料ティア | 無料ティア |
| Deploy Request | ❌ | ✅ (Vitessのみ) | ❌ | ❌ |
プロダクションCI/CD統合:完全セットアップ
PRごとにDBブランチを作り、マイグレーションを実行し、マージ時にクリーンアップするプロダクションレディなGitHub Actionsワークフローです。
ステップ1:GitHub Actionsワークフロー
# .github/workflows/preview-db.yml name: Preview Database Branch on: pull_request: types: [opened, synchronize, reopened, closed] env: NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }} NEON_API_KEY: ${{ secrets.NEON_API_KEY }} jobs: create-branch: if: github.event.action != 'closed' runs-on: ubuntu-latest outputs: db_url: ${{ steps.create.outputs.db_url }} steps: - uses: actions/checkout@v4 - name: Create Neon Branch id: create uses: neondatabase/create-branch-action@v5 with: project_id: ${{ env.NEON_PROJECT_ID }} api_key: ${{ env.NEON_API_KEY }} branch_name: pr-${{ github.event.number }} parent: main - name: Run Migrations env: DATABASE_URL: ${{ steps.create.outputs.db_url }} run: | npx drizzle-kit push echo "✅ ブランチ pr-${{ github.event.number }} にマイグレーション適用完了" - name: Comment PR with Database URL uses: actions/github-script@v7 with: script: | github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: `🗄️ **プレビューDBブランチ作成完了**\n\nブランチ: \`pr-${context.issue.number}\`\n接続情報: プレビューデプロイの環境変数で確認可能です。` }); cleanup-branch: if: github.event.action == 'closed' runs-on: ubuntu-latest steps: - name: Delete Neon Branch uses: neondatabase/delete-branch-action@v3 with: project_id: ${{ env.NEON_PROJECT_ID }} api_key: ${{ env.NEON_API_KEY }} branch: pr-${{ github.event.number }}
ステップ2:Vercel統合
VercelデプロイではNeonがプレビューデプロイごとに自動ブランチを作成するネイティブ統合を提供しています:
// vercel.json — Neon Vercel統合使用時は変更不要 // 統合が自動的に: // 1. Vercelがプレビューデプロイを作成するとNeonブランチを作成 // 2. プレビューデプロイの環境にDATABASE_URLを注入 // 3. プレビューデプロイ削除時にNeonブランチも削除 // アプリケーションコードは同じように動作: import { neon } from '@neondatabase/serverless'; const sql = neon(process.env.DATABASE_URL!); export async function getUsers() { const users = await sql`SELECT * FROM users LIMIT 10`; return users; }
ステップ3:スキーママイグレーション戦略
最も一般的なパターンはブランチ作成時にマイグレーションを実行すること:
// drizzle.config.ts import { defineConfig } from 'drizzle-kit'; export default defineConfig({ schema: './src/db/schema.ts', out: './drizzle', dialect: 'postgresql', dbCredentials: { url: process.env.DATABASE_URL!, }, });
// src/db/schema.ts — スキーマ変更がPRの一部 import { pgTable, text, timestamp, integer } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: text('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull().unique(), // このPRで追加された新しいカラム: avatarUrl: text('avatar_url'), createdAt: timestamp('created_at').defaultNow(), });
デベロッパーのワークフローはこうなります:
1. コード変更 + スキーマ変更を含むPR作成
2. GitHub Actionsが本番からDBブランチを作成
3. ブランチでマイグレーション実行(avatar_urlカラム追加)
4. VercelがブランチのDATABASE_URLでプレビューデプロイ
5. 実際の本番データに対して機能テスト(新カラム付き)
6. PRマージ → ブランチ削除、本番でマイグレーション実行
アドバンスドパターン
パターン1:デバッグのためのポイントインタイムブランチング
顧客からバグ報告が来たら、バグが発生した正確な時点のブランチを作れます:
# 2時間前の時点からブランチを作成 neonctl branches create \ --project-id my-project \ --name debug-issue-1234 \ --parent main \ --timestamp "2026-04-09T06:00:00Z" # 2時間前の正確なDB状態が手に入る # クエリして、分析して、バグを再現できる
バックアップを別インスタンスに復元するより圧倒的に優れています。ブランチは即座に作れて、コストゼロで、本番に影響しない。
パターン2:長期環境のブランチリセット
定期的なリフレッシュが必要な永続的な開発ブランチを維持するチームもいます:
# devブランチを現在の本番状態とリセット neonctl branches reset dev-environment \ --parent main # ブランチに最新の本番データが反映される # 削除して再作成する必要なし
パターン3:スキーマドリフト検出
ブランチを使って、スキーマドリフトが本番障害になる前に検出できます:
// CIジョブ:ブランチスキーマを本番と比較 import { neon } from '@neondatabase/serverless'; async function detectSchemaDrift() { const prodDb = neon(process.env.PRODUCTION_DATABASE_URL!); const branchDb = neon(process.env.BRANCH_DATABASE_URL!); const prodSchema = await prodDb` SELECT table_name, column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position `; const branchSchema = await branchDb` SELECT table_name, column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position `; const drift = findDifferences(prodSchema, branchSchema); if (drift.length > 0) { console.error('⚠️ スキーマドリフト検出:', drift); process.exit(1); } console.log('✅ スキーマドリフトなし'); }
パターン4:本番データでの負荷テスト
従来の負荷テストは合成データを使うが、実データの分布と同じクエリプランナーの動作を引き起こせません:
# 負荷テスト用ブランチ作成 neonctl branches create \ --project-id my-project \ --name load-test-$(date +%Y%m%d) \ --parent main # ブランチに対して負荷テスト実行 # 本物のインデックス、本物のデータ分布、本物のクエリプラン k6 run load-test.js --env DB_URL=$BRANCH_URL # 終わったらブランチ削除 neonctl branches delete load-test-$(date +%Y%m%d)
コスト分析:本当に安くなる?
意外かもしれませんが、データベースブランチングは従来のステージング環境よりむしろ安く済むことが多いんです。
従来のアプローチ
本番RDSインスタンス: $200/月
ステージングRDSインスタンス: $200/月 (常時起動)
開発用RDSインスタンス: $100/月 (常時起動)
合計: $500/月
ステージングインスタンスは営業時間にしか使われないのに24/7動いている。開発用インスタンスは一度に1人しか使わない。
データベースブランチングアプローチ
本番Neonインスタンス: $19/月 (ワークロードに合わせてスケール)
プレビューブランチ: ~$2/月 (Scale-to-Zero、一時的)
開発ブランチ: ~$1/月 (Scale-to-Zero)
合計: ~$22/月
Neonブランチは未使用時にゼロにスケールダウン。3時間アクティブなPRのプレビューブランチのコストは数セント。95%以上のコスト削減はアイドルリソースに金を払わないことから生まれます。
コストが跳ね上がるケース
注意すべき点:
- 書き込みの多いブランチ: CoWは書き込みごとに追加ストレージを生成
- 長期ブランチ: 親から大きく分岐したブランチはストレージが蓄積
- コンピュート時間: ブランチで高コストなクエリを継続実行するとコンピュートコストが増加
マイグレーション戦略:現状からの移行
従来のPostgreSQL(RDS、Cloud SQL、セルフホスト)で運用中なら、このパスで移行しましょう:
Phase 1:シャドウモード(1-2週目)
既存のDBと並行してNeonを動かす。本番は現プロバイダーのまま、CI/CDとプレビュー環境でのみブランチングを使用。
# .env.production — そのまま DATABASE_URL=postgres://user:pass@your-rds-instance.amazonaws.com/mydb # .env.preview — Neonブランチを使用 DATABASE_URL=${{ NEON_BRANCH_URL }}
Phase 2:デュアルライト検証(3-4週目)
本番の書き込みをNeonにミラーリングして、データ整合性とパフォーマンス特性を検証。
Phase 3:本番切り替え(5週目)
本番トラフィックをNeonに切り替え。旧DBは2週間リードオンリーのフォールバックとして維持。
Phase 4:完全ブランチングワークフロー(6週目〜)
完全なブランチングワークフローを有効化。すべてのPRがブランチを持ち、マイグレーションがブランチで実行され、マージ時にブランチがクリーンアップされる。
よくある落とし穴と回避法
落とし穴1:古いブランチ
月曜に本番から作ったブランチには金曜の変更が反映されていない。数時間以上生きるブランチは:
- ブランチリセットで親と再同期するか
- 永続的な開発環境は夜間自動再作成を設定
落とし穴2:接続文字列の管理
最もよくあるミスは環境変数ではなく接続文字列をハードコードすること:
// ❌ 絶対にやらないこと const db = new Pool({ connectionString: 'postgres://...' }); // ✅ 必ず環境変数を使う const db = new Pool({ connectionString: process.env.DATABASE_URL });
落とし穴3:孤児ブランチ
自動クリーンアップなしではブランチが溜まる。常に作成と削除をペアにすること:
# PR open/sync → ブランチ作成 # PR close → ブランチ削除 # 週次cron → 孤児ブランチクリーンアップ cleanup-orphans: runs-on: ubuntu-latest schedule: - cron: '0 3 * * 0' # 毎週日曜 午前3時 steps: - name: 古いブランチの一覧取得と削除 run: | BRANCHES=$(neonctl branches list --project-id $NEON_PROJECT_ID --output json) echo "$BRANCHES" | jq -r '.[] | select(.name | startswith("pr-")) | .id' | while read id; do neonctl branches delete $id --project-id $NEON_PROJECT_ID done
落とし穴4:ブランチ内の機密データ
ブランチには本番データのコピーが含まれる。本番DBにPIIがあれば、ブランチにもPIIがある。対策:
- ブランチ作成時にデータマスキングを適用
- 実データが不要な環境にはスキーマのみブランチング(データなし)を使用
- Row-Level Security(RLS)ポリシーを実装(ブランチにも伝播する)
落とし穴5:マイグレーションの順序
2つのPRが同時にマイグレーションを追加する場合、マージ順序が重要になる。連番ではなくタイムスタンプベースのマイグレーションファイルを使うこと(DrizzleとPrismaがデフォルトで生成する形式)。
まとめ
データベースブランチングは、チームのDB運用を根本から変えます。みんなが恐る恐る触る共有リソースじゃなくて、コードと同じようにブランチして、テストして、使い捨てるアーティファクトになるんです。
技術はもう十分成熟しています。NeonのCoWストレージは2024年から本番で安定稼働。CI/CD統合も動く。コストモデルも納得感がある。今日導入しない理由があるとすれば、それは惰性だけです。
チームがまだステージングDBを共有しているなら、もう存在しなくていい問題にお金を払い続けています。壊れたプレビュー環境も、マイグレーションの衝突も、「ステージングでは動いた」問題も、すべて払わなくていい税金です。
データベースをブランチしましょう。コードをブランチするのと同じように。すべてのPRで。毎回。
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう