マイグレーションが怖い理由
「本番DBのスキーマ変更は怖い」と感じている開発者は多いでしょう。テーブルのカラム追加程度であればリスクは低いですが、カラムの削除、リネーム、大規模なデータ移行が必要な変更では、アプリケーションの停止や最悪の場合データ損失のリスクがあります。しかし適切な戦略を用いることで、ほとんどのマイグレーションをダウンタイムなしで実施できます。
基本原則:後方互換性を維持する
最も重要な原則は「デプロイ中も古いバージョンのアプリと新しいバージョンのアプリが同時に動作できる状態を維持する」ことです。ブルーグリーンデプロイやカナリアリリースでは、新旧バージョンが同時に実行される期間があるため、データベーススキーマは両バージョンに対応している必要があります。
パターン1:エクスパンド/コントラクト(Expand/Contract)
カラムのリネームやテーブル構造の大幅変更に有効なパターンです。3段階に分けて変更を行います。
-- 例:users.full_name を first_name + last_name に分割する変更
-- === Phase 1: EXPAND(拡張)===
-- 新しいカラムを追加(古いカラムはまだ残す)
ALTER TABLE users ADD COLUMN first_name VARCHAR(100);
ALTER TABLE users ADD COLUMN last_name VARCHAR(100);
-- 既存データを移行
UPDATE users SET
first_name = SPLIT_PART(full_name, ' ', 1),
last_name = SPLIT_PART(full_name, ' ', 2)
WHERE first_name IS NULL;
-- アプリは full_name と first_name/last_name の両方に書き込む
-- === Phase 2: MIGRATE(移行)===
-- 新バージョンのアプリをデプロイ(first_name/last_nameを使用)
-- 旧バージョンのアプリを徐々に停止
-- === Phase 3: CONTRACT(縮小)===
-- 古いカラムを削除(全アプリが新バージョンになった後)
ALTER TABLE users DROP COLUMN full_name;
パターン2:段階的なインデックス作成
大規模テーブルへのインデックス追加はテーブルロックを引き起こす場合があります。PostgreSQLのCREATE INDEX CONCURRENTLYを使用することでロックを回避できます。
-- ❌ 通常のインデックス作成(テーブルをロック)
CREATE INDEX idx_orders_created_at ON orders(created_at);
-- ✅ 並行インデックス作成(テーブルロックなし)
-- ※ 時間は長くかかるが、本番環境への影響を最小化
CREATE INDEX CONCURRENTLY idx_orders_created_at ON orders(created_at);
-- インデックス作成の進捗確認
SELECT phase, blocks_done, blocks_total,
ROUND(100.0 * blocks_done / NULLIF(blocks_total, 0), 1) AS progress_pct
FROM pg_stat_progress_create_index
WHERE relid = 'orders'::regclass;
パターン3:大量データの分割マイグレーション
数百万件のデータを一度に更新するとロングトランザクションが発生し、レプリケーション遅延やロック問題を引き起こします。バッチ処理で分割して実行しましょう。
-- ❌ 悪い例:数百万件を一度に更新
UPDATE orders SET status = 'legacy_pending' WHERE status = 'pending';
-- ✅ 良い例:バッチ処理で分割実行
DO $$
DECLARE
batch_size INT := 10000;
updated_count INT;
BEGIN
LOOP
UPDATE orders SET status = 'legacy_pending'
WHERE id IN (
SELECT id FROM orders
WHERE status = 'pending'
LIMIT batch_size
FOR UPDATE SKIP LOCKED
);
GET DIAGNOSTICS updated_count = ROW_COUNT;
EXIT WHEN updated_count = 0;
-- レプリケーションに追いつく時間を与える
PERFORM pg_sleep(0.1);
RAISE NOTICE 'Updated % rows', updated_count;
END LOOP;
END $$;
パターン4:フィーチャーフラグとの組み合わせ
スキーマ変更とアプリコードの変更を完全に分離するため、フィーチャーフラグを活用します。新しいカラムやテーブルはスキーマに追加しつつ、アプリ側ではフラグが有効になるまで使用しません。
-- マイグレーション管理テーブル
CREATE TABLE schema_migrations (
version VARCHAR(14) PRIMARY KEY, -- タイムスタンプ形式:20260403120000
description TEXT NOT NULL,
applied_at TIMESTAMP DEFAULT NOW(),
rollback_sql TEXT -- ロールバック手順を記録
);
INSERT INTO schema_migrations(version, description, rollback_sql)
VALUES (
'20260403120000',
'Add first_name and last_name columns to users',
'ALTER TABLE users DROP COLUMN first_name, DROP COLUMN last_name'
);
Erdmatesでマイグレーション計画を立てる
スキーマ変更を実施する前に、ErdmatesでERDを最新化し「Before/After」を視覚的に確認することをお勧めします。変更の影響範囲(どのテーブルがどのように変わるか)をERDで整理しておくことで、チーム全体がマイグレーション計画を共有でき、手順のレビューもスムーズになります。
まとめ
データベースマイグレーションは計画的に行えば、ダウンタイムなしで安全に実施できます。エクスパンド/コントラクトパターン、並行インデックス作成、バッチ処理の3つの手法を使い分けることで、ほとんどのシナリオに対応できます。最も重要なのは、一度のデプロイで大きな変更を行うのではなく、小さな変更を段階的に積み重ねていく姿勢です。