🚧
概念 #データ設計 #マイグレーション #PostgreSQL #ゼロダウンタイム #デプロイ #DDIA 📚 データ志向アプリケーション設計(DDIA)

ゼロダウンタイムマイグレーション

本番DBをサービスを止めずに変更するExpand-Contractパターン。カラム追加・削除・リネームを安全に行うステップバイステップの手順と、大規模テーブルへの変更戦略を理解する

定義

ゼロダウンタイムマイグレーション:本番データベースのスキーマ変更を、サービスを停止せずに行う手法。デプロイとマイグレーションを分離し、旧コードと新コードが同時に動作する期間を安全に乗り越える。

なぜ単純なALTER TABLEは危険か

-- 本番DBへのナイーブな変更
ALTER TABLE users RENAME COLUMN email TO email_address;

問題1: テーブルロック
  PostgreSQLはRENAMEでAccessExclusiveLockを取得
  → その間すべての読み書きがブロックされる
  
問題2: コードとDBの不整合
  旧コード: email を参照 → エラー
  新コード: email_address を参照 → OK
  
  デプロイ中に旧コードが動いている間、DBはどちらを持つべきか?

Expand-Contract パターン

スキーマ変更を「拡張 → 移行 → 収縮」の3フェーズに分ける。

Phase 1: Expand(拡張)
  新旧両方に対応できるようDBを変更
  
Phase 2: Migrate(移行)
  データを移行し、コードを新しい構造に切り替える
  
Phase 3: Contract(収縮)
  古い構造を削除する

カラムリネームの例

目標

users.emailusers.email_address にリネームしたい

Phase 1: Expand(新カラムを追加)

-- 新しいカラムを追加(NULLを許容、デフォルト値なし)
ALTER TABLE users ADD COLUMN email_address VARCHAR(255);
// アプリコード: 両方のカラムに書く
async function updateEmail(userId: string, email: string) {
  await db.query(
    'UPDATE users SET email = $1, email_address = $1 WHERE id = $2',
    [email, userId]
  );
}

// 読み取り: 新カラムを優先、なければ旧カラム
async function getEmail(userId: string) {
  const { email, email_address } = await db.query(
    'SELECT email, email_address FROM users WHERE id = $1',
    [userId]
  );
  return email_address ?? email;
}

この段階でデプロイ。

Phase 2: Migrate(既存データを移行)

-- バックグラウンドで既存データを埋める
-- 一度に全行更新するとロックが発生するためバッチで処理
DO $$
DECLARE
  batch_size INT := 1000;
  last_id BIGINT := 0;
BEGIN
  LOOP
    UPDATE users
    SET email_address = email
    WHERE id IN (
      SELECT id FROM users
      WHERE email_address IS NULL AND id > last_id
      ORDER BY id
      LIMIT batch_size
    )
    RETURNING MAX(id) INTO last_id;
    
    EXIT WHEN NOT FOUND;
    PERFORM pg_sleep(0.01); -- DBへの負荷を分散
  END LOOP;
END $$;

-- 移行完了後、NOT NULL制約を追加
ALTER TABLE users ALTER COLUMN email_address SET NOT NULL;

アプリコードを新カラムのみ使うよう変更してデプロイ。

Phase 3: Contract(古いカラムを削除)

-- 旧コードが完全に消えてから削除
ALTER TABLE users DROP COLUMN email;

大規模テーブルへのインデックス追加

-- ❌ 通常のインデックス作成: テーブルロックが発生
CREATE INDEX ON orders (user_id);

-- ✅ CONCURRENTLY: ロックなしで作成(時間はかかるが本番安全)
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);
-- 注意: トランザクション内では使えない
-- 注意: 失敗するとINVALIDなインデックスが残る(要手動削除)

NOT NULL 制約の安全な追加

-- ❌ 危険: テーブル全行をスキャンしてロック
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;

-- ✅ 安全な手順(PostgreSQL 12以降)

-- Step1: CHECK制約として追加(バリデーションをスキップ)
ALTER TABLE users ADD CONSTRAINT users_phone_not_null
  CHECK (phone IS NOT NULL) NOT VALID;

-- Step2: バックグラウンドでバリデーション(ShareUpdateExclusiveLock, 弱いロック)
ALTER TABLE users VALIDATE CONSTRAINT users_phone_not_null;

-- Step3: これでNOT NULL制約と等価
-- PostgreSQL 12以降はVALIDATEDなCHECK制約をNOT NULLに変換できる

デフォルト値の安全な追加(PostgreSQL 11以降)

-- PostgreSQL 10以前: デフォルト値付きカラム追加は全行書き換え → 危険
ALTER TABLE orders ADD COLUMN status VARCHAR(20) DEFAULT 'pending';

-- PostgreSQL 11以降: メタデータのみ更新(全行書き換えなし)
-- 既存行は読み取り時に動的にデフォルト値を返す → 安全

pg_repack / pt-online-schema-change

完全なテーブル書き換えが必要な場合のツール。

pg_repack の動作:
  1. テーブルのコピーを作成
  2. コピーに変更を適用(カラム削除、型変更など)
  3. 元テーブルへの変更を差分としてトリガーでキャプチャ
  4. 差分を適用
  5. 極短時間のロックで名前を入れ替え

→ ダウンタイムをほぼゼロにして大規模なテーブル変更が可能

マイグレーションツールの選択

ツール特徴
Flyway / Liquibaseバージョン管理型。SQLまたはXMLで記述
golang-migrateシンプルなGoツール
Prisma MigrateTypeScriptスキーマからSQLを生成
sqitch変更の依存関係を管理

共通の注意点

  • マイグレーションは一方通行(ロールバックは別途実装)
  • 本番適用前にステージングで必ず検証
  • 大規模テーブルの変更はメンテナンスウィンドウを検討

マイグレーションの原則

原則1: アプリとDBを同時に変更しない
  DBを先に変更して後方互換を保ち、アプリをデプロイし、旧構造を削除

原則2: 後方互換を壊す変更は段階的に
  リネーム = 追加 → 移行 → 削除 の3ステップ

原則3: 本番DBで`テスト`しない
  必ずステージング環境で実行時間と影響を確認

原則4: 長時間ロックを避ける
  CONCURRENTLY、NOT VALID、pg_repackを積極的に使う

関連概念

出典・参考文献

  • Martin Kleppmann, Designing Data-Intensive Applications (2017) Chapter 4
  • Braintree Engineering, “Safe Operations For High Volume PostgreSQL” (2014)
  • PostgreSQL Documentation, “ALTER TABLE” — postgresql.org/docs/current/sql-altertable.html
  • pg_repack — github.com/reorg/pg_repack
  1. 1. 🗄️データ志向アプリケーション設計:概要
  2. 2. 🧩データモデルとクエリ言語
  3. 3. 💾ストレージエンジンとインデックス
  4. 4. 🔁レプリケーション
  5. 5. 🍕パーティショニング(シャーディング)
  6. 6. 🔒トランザクションとACID
  7. 7. 分散システムの本質的な問題
  8. 8. 🤝一貫性と分散合意
  9. 9. 📦バッチ処理
  10. 10. 🌊ストリーム処理
  11. 11. 📋エンコーディングとスキーマ進化
  12. 12. 🔗Sagaパターンと分散トランザクション
  13. 13. 🏗️データシステムの統合設計
  14. 14. 📸MVCC(多版型同時実行制御)
  15. 15. 📊列指向ストレージとOLAP設計
  16. 16. 🕰️ベクタークロックと因果順序
  17. 17. 🔀CRDT(競合なし複製データ型)
  18. 18. 🔍クエリオプティマイザーと実行計画
  19. 19. キャッシュ戦略とRedis設計
  20. 20. 🔎全文検索と転置インデックス
  21. 21. 🌐NewSQL(分散ACIDデータベース)
  22. 22. 📝WALと論理レプリケーション
  23. 23. 🔌コネクションプーリング
  24. 24. 🚧ゼロダウンタイムマイグレーション
  25. 25. 🆔分散ID生成
  26. 26. 🔄N+1問題とDataLoaderパターン
  27. 27. 📈タイムシリーズDB
  28. 28. 🛡️Row Level Security(行レベルセキュリティ)
  29. 29. 📤Outboxパターン(トランザクショナルアウトボックス)
  30. 30. 💾DBバックアップとPITR
  31. 31. ⚠️データベース設計アンチパターン
  32. 32. 🕸️グラフDB深掘り
  33. 33. 🔋バックプレッシャーとサーキットブレーカー

出典: Martin Kleppmann, 'Designing Data-Intensive Applications' (2017) Chapter 4