コンテンツにスキップ

本番DBへのサービス無停止スキーマ変更:3年分の決済レコードを抱えたままETC対応した実例

概要

バクラクビジネスカードのクレカ専用 DB を ETC 対応に拡張した際、約3年分の決済レコードを抱える本番 DB に対してサービス無停止でスキーマ変更した実例。

詳細

なぜ本番 DB のスキーマ変更は難しいか

問題:
  ALTER TABLE に LOCK がかかる → 書き込みがブロックされる
  大テーブルへの NOT NULL 制約追加 → 全行の更新が必要
  カラム追加でも古いアプリコードとの互換性が必要

ゼロダウンタイム スキーマ変更のパターン

Expand-Contract パターン(推奨)

Phase 1: Expand(拡張)
  → 新しいカラムを nullable で追加
  → 既存のコードは古いカラムのまま動作(後方互換)

Phase 2: Migrate(移行)
  → バックグラウンドで既存行の新カラムを埋める
  → バッチで少しずつ(全行を一括更新しない)

Phase 3: Switch(切り替え)
  → アプリコードを新カラムに切り替える
  → 古いカラムへの書き込みを止める

Phase 4: Contract(縮退)
  → 古いカラムを削除
  → (安全を確認してから、別デプロイで実施)

バッチ更新で大テーブルを安全に更新する

-- 悪い例: 全行を一括更新 → テーブルロック
UPDATE payments SET payment_type = 'credit_card' WHERE payment_type IS NULL;

-- 良い例: 少しずつ更新(Lock を長時間保持しない)
DO $$
DECLARE
  batch_size INT := 1000;
  offset_val INT := 0;
  rows_updated INT;
BEGIN
  LOOP
    UPDATE payments SET payment_type = 'credit_card'
    WHERE id IN (
      SELECT id FROM payments WHERE payment_type IS NULL
      ORDER BY id LIMIT batch_size
    );
    GET DIAGNOSTICS rows_updated = ROW_COUNT;
    EXIT WHEN rows_updated = 0;
    PERFORM pg_sleep(0.1);  -- 少し待つ
  END LOOP;
END $$;

インデックスの安全な追加

-- 悪い例: テーブルをロック
CREATE INDEX idx_payments_user_id ON payments(user_id);

-- 良い例: 並行ビルド(ロックなし、時間はかかる)
CREATE INDEX CONCURRENTLY idx_payments_user_id ON payments(user_id);

なぜ重要か / いつ使うか

  • 本番サービスを止めずに DB スキーマを変更する必要があるとき
  • 数百万〜数億行のテーブルを安全に変更するとき
  • マイグレーションの設計をレビューするとき
  • 面接で「大規模 DB のマイグレーション」を聞かれたとき