データベースマイグレーション戦略

本番環境のデータベーススキーマを安全に変更するための戦略とパターン

データベース運用

データベースマイグレーション戦略とは

データベースマイグレーションは、本番環境のスキーマ (テーブル構造、インデックス、カラム) を安全に変更するプロセスである。アプリケーションのデプロイと異なり、ロールバックが困難で、データ損失のリスクがある。

マイグレーションツール

マイグレーションツールを以下に示す。

ツール言語/フレームワーク方式
Prisma MigrateTypeScript宣言的 (スキーマ差分)
FlywayJavaバージョン管理 (SQL ファイル)
Knex.jsNode.jsプログラマティック
AlembicPython (SQLAlchemy)バージョン管理
golang-migrateGoSQL ファイル

Expand-Contract パターン

破壊的変更を安全に行うための 3 段階のパターン。

Phase 1 (Expand): 新カラムを追加、新旧両方に書き込み
  ALTER TABLE users ADD COLUMN email_v2 VARCHAR(255);
  → アプリは old_email と email_v2 の両方に書き込む

Phase 2 (Migrate): 既存データを移行
  UPDATE users SET email_v2 = old_email WHERE email_v2 IS NULL;

Phase 3 (Contract): 旧カラムを削除
  ALTER TABLE users DROP COLUMN old_email;
  → アプリは email_v2 のみ使用

各フェーズを別々のデプロイで行い、問題があれば途中で止められる。

このパターンで詰まりやすいのは 2 点ある。1 つは Phase 2 で、行数の多いテーブルに UPDATE を 1 文で流すと長いトランザクションと大量の更新ログ・不要行を生むため、主キーの範囲で区切って少しずつ回す。もう 1 つは読み取りの切り替えで、旧カラムを削除できるのは「新カラムだけを読むコード」が全インスタンスに行き渡り、旧カラムを参照する箇所がどこにも残っていないと確認できてからだ。書き込みの二重化と読み取りの切り替えを同じデプロイに詰め込むと、片方だけ戻したときに不整合が残る。

DynamoDB のマイグレーション

DynamoDB でスキーマレスなのはプライマリキー以外の属性で、属性名も型も事前に定義せずに済む。逆に言えば、パーティションキーとソートキー、そしてインデックスのキーに使う属性は設計として決まっているため、データ構造の変更はアプリケーション側で吸収することになる。

// バージョンフィールドで新旧フォーマットを共存
async function getUser(id: string) {
  const { Item } = await db.get({ TableName: 'users', Key: { id } });
  if (!Item) return null;
  // schemaVersion が無い項目は移行前 (v1) として扱う
  if ((Item.schemaVersion ?? 1) < 2) {
    return migrateV1toV2(Item); // 読み取り時に変換
  }
  return Item;
}

DynamoDB では「読み取り時に変換 + 書き戻し」という遅延移行が一般的で、全件を止めて一括変換しなくても移行を始められる。ただし読まれない項目は旧形式のまま残るので、変換コードをいつか撤去したいなら、どこかで残りをスキャンして書き戻す作業が必要になる。新しく足した属性をキーにしてグローバルセカンダリインデックスを張る場合はさらに注意が必要で、この種のインデックスはキー属性が実際に存在する項目だけを追跡する (属性を持たない項目は載らない) ため、書き戻しが終わるまでインデックスは全項目を含まない。

安全なマイグレーションのルール

安全なマイグレーションのルールを以下にまとめる。

ルール理由
カラム追加は安全既存のクエリに影響しない
カラム削除は危険アプリが参照していると障害
カラム名変更は危険実質的に削除 + 追加
NOT NULL 追加は危険既存データが制約違反になる (追加時に全行を確認する)
インデックス追加は注意構築中に書き込みが止まる場合がある

危険度は実装の挙動で決まるので、PostgreSQL 17 を例に補っておく。ALTER TABLE は明示的に注記のある一部を除き ACCESS EXCLUSIVE ロックを取るため、短時間で済むかどうかはテーブルの書き換えが起きるかで分かれる。デフォルト値付きのカラム追加は、そのデフォルトが volatile でなければ値をテーブルのメタデータに保存するだけで、テーブル全体の書き換えは起きない。NOT NULL の追加は通常テーブル全体を走査して確認するが、NULL がないことを示せる有効な CHECK 制約が既にあれば走査を省略する。インデックス追加は、通常の CREATE INDEX が完了まで書き込みをロックする (読み取りは通る) のに対し、CREATE INDEX CONCURRENTLY は挿入・更新・削除を止めずに構築できる。他のデータベースでも同種の仕組み (MySQL のオンライン DDL 等) があるので、使っている版のドキュメントで確認してから流すのが安全だ。

ロールバック戦略

  • マイグレーション前にスナップショットを取得する。ただし復元は取得時点まで巻き戻るため、その後の書き込みは失われる。原則は前方修正で、スナップショットは最後の手段だ
  • Expand-Contract パターンで段階的に変更する。各フェーズが単体で戻せる形になっているかを事前に確認する
  • ダウンマイグレーション (逆方向の SQL) を用意する。構造は戻せてもカラム削除で失ったデータは戻らないので、戻せる範囲を把握しておく
  • DynamoDB は Point-in-Time Recovery (PITR) を有効化する。復元可能期間は 1 日から 35 日の範囲で設定でき、復元は常に新しいテーブルとして作られる (同名テーブルへの上書きではない) ため、切り替え手順まで決めておく

理論と実装の両面から学ぶなら関連書籍が参考になる。

この記事は役に立ちましたか?

関連用語

関連する記事