既存アプリケーションを停止せずに新しいデータベース構造を追加する

users テーブルに full_name カラムがあるとします。プロダクトチームは、より良いパーソナライゼーションのために名前を first_namelast_name に分割したいと考えています。この変更を、アプリケーションを停止したり、既存の機能を壊したりせずに行う必要があります。

単純な方法は、full_name を削除し、2つの新しいカラムを追加して、すべてのコードを一度に更新することです。しかし、それには連携したデプロイ、ダウンタイム、そして何か問題が発生した場合の完璧なロールバック計画が必要です。実際には、この種の変更は深夜のロールバックと怒れるユーザーにつながることがよくあります。

より安全な方法があります。古い構造には触れずに、最初に新しい構造を追加するのです。完全に切り替える準備ができるまで、古い構造と新しい構造を共存させます。

Expandフェーズ:削除せずに追加する

Expand-Contractパターンは、最も安全なステップから始まります。既存のすべてを完全にそのままにして、新しいカラム、テーブル、または制約をデータベースに追加します。古いスキーマに対して実行されている古いアプリケーションは、何の違いにも気づきません。新しいコードは、すぐに新しい構造を使い始めることができます。

これがExpandフェーズの核となる考え方です。既存のものを変更したり削除したりせずに、新しいデータベースオブジェクトを導入します。古いスキーマは完全に機能し続けます。新しいスキーマは置き換えではなく、追加です。

以下の図は、Expandフェーズ中のusersテーブルの変更前と変更後の状態、および新旧のアプリケーションがスキーマとどのように相互作用するかを示しています。

flowchart TD subgraph Before[Expandフェーズ前] T1[users テーブル] C1[full_name VARCHAR] end subgraph After[Expandフェーズ後] T2[users テーブル] C2[full_name VARCHAR] C3[first_name VARCHAR NULL] C4[last_name VARCHAR NULL] end OldApp[旧アプリケーション] -->|読み取りのみ| C2 NewApp[新アプリケーション] -->|読み取り/書き込み| C2 NewApp -->|読み取り/書き込み| C3 NewApp -->|読み取り/書き込み| C4 Before -->|ALTER TABLE ADD COLUMN| After

具体的な例

full_name カラムを持つ users テーブルを例にとります。Expandフェーズでは、2つの新しいカラムを追加します。

新しいカラムを追加するSQLは次のとおりです。

ALTER TABLE users ADD COLUMN first_name VARCHAR(100) NULL;
ALTER TABLE users ADD COLUMN last_name VARCHAR(100) NULL;

full_name カラムはそのまま残ります。full_name を読み取る古いアプリケーションは、コードを変更することなく引き続き動作します。新しいアプリケーションは、first_namelast_name への書き込みを開始できますが、後方互換性のために full_name も引き続き読み取ることができます。

ダウンタイムはありません。連携したリリースも不要です。既存のクエリが壊れるリスクもありません。

重要なルール:新しいカラムはオプションである必要がある

Expandフェーズで最もよくある間違いは、NOT NULL でデフォルト値のない新しいカラムを追加することです。これにより、新しいカラムを指定せずに行を挿入するアプリケーションが即座に壊れます。カラムの存在を知らない古いアプリケーションは、すべての挿入で失敗します。

ルールは単純です。すべての新しいカラムはNULL許容であるか、適切なデフォルト値を持つ必要があります。NOT NULL 制約が必要な場合は、最初にカラムをNULL許容で追加し、データをバックフィルしてから、後のフェーズで制約を追加します。既存のすべてのアプリケーションに、同時にINSERT文の変更を強制してはいけません。

同じロジックが新しいテーブルにも適用されます。新しいテーブルは既存の構造を変更しません。古いアプリケーションはその存在を知る必要がありません。新しいアプリケーションはすぐに書き込みを開始できます。既存のテーブルには何も変更がないため、競合は発生しません。

制約を安全に扱う

Expandフェーズでは、制約には特に注意が必要です。新しいカラムに UNIQUE 制約を追加する場合は、既存のデータがそれに違反していないことを確認してください。FOREIGN KEY を追加する場合は、既存のすべての行が有効な親行を参照していることを確認してください。

CHECK 制約の場合は、条件が既存のデータと競合しないことを確認してください。一部のデータベースは NOT VALID オプションをサポートしており、制約を新しい行にのみ適用し、既存のデータは後で個別に検証できます。これは、古い行のデータ品質に確信が持てない場合に便利です。

原則は同じです。既存のデータに対して失敗する制約を導入してはいけません。保証できない場合は、制約を延期するか、書き込みをブロックしない方法で追加します。

命名の重要性

新しいカラムとテーブルには、古い構造と区別できる明確な名前が必要です。name_newtemp_namename_v2 のような名前は避けてください。これらは、どの構造が正規のものかを判断する必要があるContractフェーズで混乱を引き起こします。

実際のデータを説明する名前を使用してください。name_split_1name_split_2 よりも first_namelast_name の方が優れています。適切な名前は、後でスキーマを扱うすべての人の移行を容易にします。

Expandフェーズに必要なもの

Expandフェーズでは、古いアプリケーションのコード変更は必要ありません。古いアプリケーションは、同じスキーマ、同じクエリ、同じロジックを使い続けます。これが、Expandフェーズをいつでも、ピーク時の本番環境でも安全に実行できる理由です。

ダウンタイムはありません。アプリケーションの再起動も不要です。カラムが突然消えてクエリが失敗することもありません。変更されるのはデータベーススキーマのみであり、その変更は純粋に追加的なものです。

Expandフェーズが完了したとき

Expandフェーズは、新しい構造がデータベースに存在し、使用できる状態になったときに完了します。古いアプリケーションは引き続き古い構造を使用します。新しいアプリケーションは新しい構造を使い始めることができます。両方のパスが同時に機能します。

この時点で、データベースは2つのバージョンのスキーマをサポートしています。これは次のステップ、つまり何も壊さずにアプリケーションを徐々に新しい構造に移行するための基盤です。

Expandフェーズの実践的なチェックリスト

  • 新しいカラムはNULL許容であるか、デフォルト値を持つ
  • 新しいテーブルは既存のテーブル構造を変更しない
  • 新しい制約は既存のデータと競合しない
  • 名前は新しい構造と古い構造を明確に区別する
  • 古いアプリケーションはコード変更なしで動作し続ける
  • 新しいアプリケーションはすぐに新しい構造を使い始められる

まとめ

Expandフェーズは、追加のみを行うため、最も安全なデータベース変更です。削除も変更もなく、実行中のアプリケーションを壊すリスクもありません。新しいカラムはNULL許容で追加し、古いカラムはそのまま維持し、両方のスキーマを共存させます。これにより、ビッグバンリリースを調整したりダウンタイムをスケジュールしたりすることなく、アプリケーションを自分のペースで移行する自由が得られます。