本番環境を壊さずにデータベーススキーマを変更する方法

あなたのチームが扱うデータベースは、5年、10年、あるいは15年もの間稼働し続けているかもしれません。そこには数百万件のトランザクション、数千ものテーブル、そしてすでに退職したエンジニアが書いた数百ものストアドプロシージャが眠っています。新しいカラムを追加する、データ型を変更する、インデックスを修正する——そのたびに必ず浮かぶ疑問があります。「このマイグレーションが失敗したら、復旧にどれくらい時間がかかるのか?」

このような組織では、データベースは単なるデータ保存領域ではありません。ビジネスの心臓部です。アプリケーションが落ちればユーザーは待つことができますが、データベースが破損すればデータは永久に失われる可能性があります。だからこそ、スキーマやデータの変更は高リスクな作業として扱われ、土曜の深夜2時にスケジュールされ、月曜の朝まで誰も気づかないことを願う——そんな運用が当たり前になっています。

しかし、このアプローチはスケールしません。チームがより速く機能をリリースしようとすればするほど、データベース変更の頻度は増えます。すべての変更を毎週のメンテナンスウィンドウまで待たなければならないとなると、プロダクトチームは不満を募らせます。かといって、変更をいい加減に行えば、データ破損のリスクは現実のものとなります。

本当に問うべきは「どのマイグレーションツールが最適か」ではありません。そうではなく、「サービスを停止せずにデータベーススキーマを変更するにはどうすればよいか、そして何か問題が起きたときにどうやって元に戻すか」です。

安全なマイグレーションは小さなステップから

基本原則はシンプルです。すべての変更は、稼働中のアプリケーションとの接続を断たずに実行可能でなければならず、データを失うことなくロールバック可能でなければなりません。つまり、スキーマ変更は一度の大きなジャンプではなく、複数の小さなステップに分割する必要があります。

新しいカラムを追加するケースを考えてみましょう。レガシーデータベースでは、デフォルト値を設定するかNULL許容でカラムを追加します。すぐに厳格な制約を追加してはいけません。古いバージョンのアプリケーションインスタンスは新しいカラムを読み込まないため、そのまま動作し続けます。新しいアプリケーションインスタンスはそのカラムに書き込みを開始します。すべてのインスタンスが更新され、安定して動作したことを確認した後、NOT NULL制約や外部キーなどの制約を別のマイグレーションで追加します。途中で問題が発生した場合、新しいカラムを無視するだけでロールバックできます。テーブルを削除したり、バックアップからリストアする必要はありません。

以下のシーケンス図は、上記で説明した安全な段階的プロセスを示しています。

sequenceDiagram participant OldApp as Application (旧バージョン) participant DB as Database participant NewApp as Application (新バージョン) Note over DB: Step 1: NULL許容カラムを追加 DB->>DB: ALTER TABLE ADD COLUMN nullable Note over OldApp,DB: Step 2: 旧アプリは影響を受けずに継続 OldApp->>DB: 読み取り/書き込み(新しいカラムは無視) DB-->>OldApp: 応答 Note over DB,NewApp: Step 3: 新しいカラムを使用する新アプリをデプロイ NewApp->>DB: 新しいカラムに書き込み DB-->>NewApp: OK NewApp->>DB: 新しいカラムから読み取り DB-->>NewApp: データ Note over DB: Step 4: 制約を追加 DB->>DB: ALTER TABLE ADD NOT NULL Note over OldApp: Step 5: 旧アプリを削除 OldApp-->>OldApp: 廃止

同じパターンはデータ型の変更にも適用できます。例えば、priceカラムが現在INTEGER型で、それをDECIMAL型に変更する必要があるとします。安全なアプローチは次の通りです。price_decimalという新しいカラムを追加し、古いカラムから変換した値を投入します。アプリケーションは新しいカラムから読み取りつつ、両方のカラムに書き込みを続けます。すべてが安定したら、古いカラムを削除します。ロールバックする場合は、アプリケーションが再び古いカラムから読み取るようにすればよく、古いカラムはまだ存在しています。

以下のSQL例は、カラムを安全に追加するためのフォワードマイグレーションとロールバックスクリプトを示しています。

-- Forward migration 1: NULL許容でカラムを追加
ALTER TABLE products ADD COLUMN discount_rate DECIMAL(5,2) NULL;

-- バックフィル(アプリケーションが新しいカラムに書き込みを開始した後に実行)
UPDATE products SET discount_rate = 0.00 WHERE discount_rate IS NULL;

-- Forward migration 2: NOT NULL制約を追加
ALTER TABLE products ALTER COLUMN discount_rate SET NOT NULL;

-- ロールバックスクリプト(両方のステップを元に戻す)
ALTER TABLE products ALTER COLUMN discount_rate DROP NOT NULL;
ALTER TABLE products DROP COLUMN discount_rate;

複雑な変更にはパラレルランを

1つのテーブルを2つに分割する、あるいは複数のテーブルをマージするといった、より複雑な変更には「パラレルラン」と呼ばれる手法を用います。アプリケーションは新旧両方の構造に同時に書き込みを行い、読み取りクエリは徐々に新しい構造に移行します。チームは両方の構造からの結果を比較し、データの差異がないことを確認できます。もし異常が検出された場合、データを失うことなくアプリケーションを古い構造に切り戻すことができます。

このアプローチには、アプリケーション側での注意深いコーディングが必要です。アプリケーションは両方の構造を認識し、両方への書き込みを処理できなければなりません。また、どちらの構造から読み取るかを決定するロジックも必要です。これは簡単な作業ではありませんが、一発勝負のビッグバンマイグレーション(成功するか大規模インシデントを引き起こすかのどちらか)を行うよりははるかに安全です。

マイグレーションはスキーマだけでなくデータが本題

よくある間違いは、データベースマイグレーションを純粋なスキーマ操作として扱うことです。マイグレーション後も、既存のデータは一貫性を保たなければなりません。すべてのマイグレーションには、フォワードスクリプトとロールバックスクリプトの2つが必要です。ロールバックスクリプトは、単にフォワードスクリプトの逆操作を実行するだけでは不十分です。マイグレーション前とまったく同じ状態にデータを戻さなければなりません。これには、マイグレーション処理中にアプリケーションによって変更された可能性のあるデータも含まれます。

例えば、マイグレーションでカラム名を変更し、その値を変換する場合、ロールバックスクリプトはカラム名と値の変換の両方を元に戻す必要があります。マイグレーションウィンドウ中にアプリケーションが新しい名前のカラムに新しいデータを書き込んでいた場合、ロールバックスクリプトはそのデータを単に削除するのではなく、正しく処理しなければなりません。

パイプラインにおけるマイグレーションの位置づけ

CI/CDパイプラインにおいて、データベースマイグレーションはアプリケーションデプロイとは独立して実行される別のステージであるべきです。パイプラインは、新しいコードのデプロイと同時にマイグレーションを実行してはいけません。代わりに、マイグレーションを先に実行します。マイグレーションが成功したことを確認した後、新しいアプリケーションバージョンをデプロイします。マイグレーションが失敗した場合、アプリケーションに影響が出る前にパイプラインは停止し、チームに通知が届きます。

この分離は非常に重要です。マイグレーションとデプロイを同時に行い、何か問題が発生した場合、その原因がスキーマ変更なのか新しいコードなのかを特定するのが難しくなります。これらを順次実行することで、各障害の原因を明確に特定できます。

手動承認が必要なケース

長期間運用されている組織では、通常、次のようなシンプルなルールを採用しています。データを変更するマイグレーション(スキーマのみの変更ではないもの)には手動承認が必要です。NULL許容カラムの追加のような、追加のみのスキーママイグレーションは自動実行できます。これは自動化が信頼できないからではなく、データ変更には構造変更よりも予測が難しい影響が伴うからです。

新しいNULL許容カラムは何も壊しません。しかし、数百万行を更新したり、値を変換したり、テーブルをマージするマイグレーションは、特定のデータ条件下でのみ現れる微妙なバグを引き起こす可能性があります。そのようなマイグレーションの前に人間によるレビューを挟むことは、ボトルネックではなく、セーフティネットです。

安全なデータベースマイグレーションのための実践的チェックリスト

  • カラムは最初にNULL許容またはデフォルト値付きで追加し、後から制約を追加する。
  • データ型を変更する場合は、新しいカラムを追加し、データを投入し、読み取りを段階的に切り替える。
  • 複雑な再構成の場合は、新旧両方の構造を並行稼働させ、結果を比較する。
  • データをマイグレーション前の正確な状態に戻すロールバックスクリプトを常に作成する。
  • マイグレーションはアプリケーションデプロイの前に実行し、同時には実行しない。
  • データを変更するマイグレーションには手動承認を必須とし、追加のみのスキーママイグレーションは自動実行を許可する。

まとめ

データベースマイグレーションは恐れる必要はありません。鍵となるのは、各変更を小さく、ロールバック可能なステップに分割することです。削除する前に追加する。切り替える前に新旧を並行稼働させる。そして、バックアップからのリストアに依存しない復旧手段を常に用意しておく。すべてのマイグレーションを安全でテスト可能な一連のステップとして扱うことで、恐怖を取り除き、データベース変更をデリバリープロセスのごく普通の一部にすることができます。