追加型データベース変更:本番環境を壊さずに追加する方法
数千のアクティブユーザーがいるアプリケーションが稼働している。チームはユーザープロフィールに電話番号フィールドを追加する必要がある。一見小さな変更に見えるが、本番データベースで ALTER TABLE を実行することを考えると誰もが緊張する。マイグレーションがテーブルをロックしたらどうなるのか?新しいカラムを想定していない古いアプリケーションコードがクラッシュしたらどうなるのか?
この状況は、自社でデータベースを管理するほぼすべてのエンジニアリングチームで発生する。良いニュースは、スキーマ変更のカテゴリの一つが、本番負荷下でも驚くほど安全であることだ。このカテゴリを理解することで、デプロイ計画の立て方、チームとの連携方法、データベース進化時のリスク負担の仕方が変わる。
追加型変更とは何か
追加型変更とは、既存のものを変更または削除せずに、データベーススキーマに新しいものだけを追加する変更のことである。具体的には以下の通り:
- 既存テーブルへの新しいカラムの追加
- 新しいテーブルの作成
- 新しいインデックスの追加
- 既存データを制限しない制約の追加
重要な特性は、スキーマを拡張しているという点である。カラムの名前変更、データ型の変更、テーブルのマージ、何かの削除は行わない。スキーマの古い部分はそのまま残る。
なぜ追加型変更が安全なのか
安全性は単純な事実に基づく:古いアプリケーションコードは、新しく追加したものに依存していない。users テーブルに phone カラムを追加しても、既存のアプリケーションコードはこれまで通り name、email、password を読み書きし続ける。新しいカラムの存在を知らない。読み取ろうともせず、書き込もうともしない。何も壊れない。
これは他のタイプの変更には当てはまらない。カラム名を phone から phone_number に変更すると、古いアプリケーションコードは phone を読み取ろうとして失敗する。なぜならそのカラムはもう存在しないからだ。カラム型を VARCHAR から INT に変更すると、古いコードがデータベースで受け付けられない文字列値を書き込む可能性がある。2つのテーブルをマージすると、元のテーブル構造を参照する古いクエリが壊れる。
追加型変更はこれらの問題をすべて回避する。古いコードが依存するものには一切触れないからだ。
唯一の重要なルール
追加型変更を安全にするための条件が一つある:新しいカラムは NULL 許容か、デフォルト値を持つ必要がある。
NOT NULL かつデフォルト値なしでカラムを追加すると、テーブルの既存のすべての行が即座に制約違反となる。データベースはマイグレーションを拒否する。最悪の場合、マイグレーションが途中まで実行され、一部の行では成功した後で失敗し、クリーンアップが困難な不整合データが残る。
標準的なパターンは次の通り:
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NULL;
最終的にカラムが必須になることがわかっている場合は、最初にデフォルト値を追加する:
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NOT NULL DEFAULT '';
デフォルト値があれば、既存のすべての行に空文字列が設定される。古いアプリケーションコードでもそのカラムをエラーなく読み取れる。後で、すべてのアプリケーションインスタンスが新しいカラムを適切に処理するように更新されたら、別のマイグレーションでデフォルトを削除するか NOT NULL 制約を追加する。
本番時間帯に追加型変更を実行する
追加型変更の最大の実用的利点の一つは、本番がビジーな状態でも実行できることだ。最新のデータベースのほとんどでは、NULL 許容カラムの ALTER TABLE ADD COLUMN は軽量な操作であり、長時間テーブルをロックしない。
例外もある。MySQL の古いバージョンでは ALTER TABLE 中にテーブルをロックする可能性がある。AFTER を使って特定の位置にカラムを追加すると、末尾に追加するよりもロックが発生しやすい。しかし一般的に、NULL 許容カラムの追加は、稼働中のデータベースに対して実行できる最も安全な操作の一つである。
つまり、メンテナンスウィンドウは不要だ。トラフィックが少ない時間帯を待つ必要もない。日中にマイグレーションを実行し、動作を確認して、デプロイの次のステップに進むことができる。
段階的デプロイを可能にする方法
追加型変更は、他のタイプのスキーマ変更では実現が難しいデプロイ戦略を可能にする:調整なしのローリングアップデートだ。
ロードバランサの背後に10のアプリケーションインスタンスがあるとしよう。phone カラムを読み書きする新しいバージョンをデプロイしたい。プロセスは次のようになる:
phoneカラムを追加するマイグレーションを実行する。スキーマに新しいカラムが追加されるが、10のインスタンスはすべて、それを無視する古いコードを実行し続ける。- 1つのインスタンスを新しいコードに更新する。そのインスタンスは
phoneカラムの読み書きを開始する。残りの9つのインスタンスは、新しいカラムに一切触れないため、正常に動作し続ける。 - 残りのインスタンスを1つずつ段階的に更新する。このプロセスのどの時点でも、古いインスタンスと新しいインスタンスは競合なく共存する。
これが可能なのは、スキーマ変更が追加型だからである。変更がカラムの削除や名前変更を伴う場合、マイグレーションが実行された瞬間に古いインスタンスが壊れる。すべてのインスタンスを一度に更新せざるを得なくなり、リスクが高まり、慎重な調整が必要になる。
追加型変更を超えるタイミング
追加型変更は安全だが、常に十分とは限らない。最終的には、未使用のカラムを削除したり、パフォーマンス問題を修正するためにデータ型を変更したり、新機能をサポートするためにテーブルを再構築したりする必要が生じる。これらの変更はより多くのリスクを伴い、異なる戦略が必要になる。
実践的な順序は次の通り:新しいカラムとテーブルを導入するために追加型変更から始め、アプリケーションコードを追いつかせ、その後、より侵襲的な変更を別のマイグレーションで計画する。各マイグレーションは1つのことだけを行い、それ以外は行わない。同じマイグレーションスクリプトに追加型変更と破壊的変更を混在させてはならない。
追加型変更のクイックチェックリスト
本番で追加型変更を実行する前に、以下のチェックを実施する:
- 新しいカラムは NULL 許容か、デフォルト値を持っているか?
- マイグレーションはものの追加のみで、既存のスキーマを変更または削除しないか?
- 本番データのコピーでマイグレーションをテストしたか?
- ロールバック計画はあるか?(追加型変更の場合、ロールバックは通常
ALTER TABLE DROP COLUMNだが、動作することを確認すること。) - マイグレーション後も古いアプリケーションコードが変更なしで実行できるか?
すべてに「はい」と答えられるなら、自信を持ってマイグレーションを実行する準備ができている。
具体的な要点
追加型変更は、データベーススキーマ変更の中で最も安全なカテゴリである。古いコードが依存するものを壊さずにスキーマを拡張するからだ。NULL 許容カラムまたはデフォルト値を使用し、通常の時間帯に実行し、アプリケーションインスタンスを段階的にデプロイする。このアプローチにより、データベースの進化と本番の安定性維持の間の緊張が解消される。すべてのスキーマ変更は「これを追加型にできるか?」と問いかけることから始める。もし可能なら、まずそれを実行する。リスクの高い変更は、新しいスキーマがすでに使用され、古いコードが廃止された後に行う。