継続的なアプリケーション成長のためのデータベーススキーマの進化
Emily Parker
Product Engineer · Leapcell

はじめに
ソフトウェア開発のペースの速い世界では、アプリケーションが静的であることは稀です。新しい機能、変化するビジネス要件、ユーザーフィードバックに牽引され、常に進化しています。アプリケーションの重要なコンポーネントであるデータベーススキーマは、しばしばこれらの変更の主な影響を受けます。従来、スキーマの変更は危険を伴い、多くの場合、ダウンタイム、複雑なマイグレーションスクリプト、そしてデータ損失やサービス中断の常に存在するリスクを必要としました。この課題は、継続的デリバリーと高可用性を目指すアプリケーションにとって特に深刻になります。アクティブなサービスに影響を与えることなく、データベーススキーマ(列の追加、変更、削除)を優雅に進化させる能力は、もはや贅沢ではなく、アジリティを維持し、中断のないユーザーエクスペリエンスを確保するための基本的な要件です。この記事では、シームレスなスキーマ進化を可能にし、アプリケーションと共にデータベースを成長させ、適応させるための戦略とテクニックを掘り下げます。
スキーマ進化の状況を理解する
実践に入る前に、データベーススキーマ進化に関連するいくつかのコアコンセプトを理解することが不可欠です。
スキーマバージョニング: コードにバージョンがあるのと同様に、データベーススキーマにもバージョンを持たせることができます。スキーマバージョニングシステムは、時間の経過とともにデータベース構造の変更を追跡し、自動化されたマイグレーションとロールバックを可能にします。FlywayやLiquibaseのようなツールは、スキーマバージョンを管理するための一般的な選択肢です。
後方互換性: データベースの変更が後方互換性を持つとは、古いバージョンのアプリケーションが新しいスキーマと正しく相互作用できる場合を指します。これは、アプリケーションのデプロイメントが段階的であったり、移行中に複数のバージョンのアプリケーションが共存したりする環境において重要です。
前方互換性: データベースの変更が前方互換性を持つとは、新しいバージョンのアプリケーションが古いスキーマと正しく相互作用できる場合を指します。これは一般的に達成するのが難しく、あまり強調されないことが多いですが、新しいスキーマを維持しながら新しいアプリケーションバージョンをロールバックするようなシナリオで関連性がある場合があります。
ゼロダウンタイムデプロイメント(ZDD): データベーススキーマ変更の究極の目標は、サービスの中断なしにそれらを実行することです。これには、古いアプリケーションコードと新しいアプリケーションコードが進化するスキーマと共存できるように、慎重に構成されたステップが含まれることがよくあります。
オンラインスキーママイグレーション: これは、データベースがオンラインで、読み書き操作にアクセス可能である間にデータベーススキーマを変更できる能力を指します。最新のデータベースシステムは、これを容易にする機能を提供していることが多いですが、慎重な計画は still 必要です。
新しい列を追加するための戦略
一般的に、新しい列の追加は最も安全なスキーマ変更ですが、サービスへの影響を避けるためには慎重な検討が必要です。
最も簡単なアプローチは、NULL可能な列を追加することです。これは、既存のアプリケーションコードが新しい列を単純に無視するため、本質的に後方互換性があります。
-- 例: 'users' テーブルに NULL 可能な 'email' 列を追加する ALTER TABLE users ADD COLUMN email VARCHAR(255);
新しい列が NULL 不可能な必要がある場合、プロセスはより複雑になります。
-
NULL 可能な列を追加する: 上記のように
ALTER TABLE ADD COLUMN
操作を実行します。ALTER TABLE users ADD COLUMN email VARCHAR(255);
-
新しいアプリケーションコードをデプロイする: アプリケーションを更新して、新しい列への書き込みを開始します。新しいコードが既存のレコードを読み取る際に、初期の
NULL
値を処理できるようにします。 -
既存のデータをバックフィルする(オプションですが、よく必要): 新しい列に既存の行のデータを入力する必要がある場合、別のスクリプトまたはバックグラウンドジョブを実行して入力します。これにより、テーブルを長期間ロックすることを避けるために、バッチで実行できます。
-- 例: 既存のユーザーのメールをバックフィルする(簡略化) UPDATE users SET email = 'default@example.com' WHERE email IS NULL;
-
列を NULL 不可能に変更する: すべての既存データがバックフィルされ、新しいアプリケーションコードが安定したら、列を NULL 不可能にすることができます。このステップは、データベースシステムによっては、テーブルに短いロックが必要になる場合があります。
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
列の変更のためのテクニック
既存の列を変更することは、特にデータ型や制約の変更が含まれる場合、より困難になる可能性があります。鍵は、移行中に後方互換性を維持することです。
列の名前を変更する: 列を直接名前変更すると、既存のアプリケーションコードが破損する可能性があります。一般的なアプローチは、多段階プロセスを伴います。
- 希望する名前と型で新しい列を追加する:
ALTER TABLE products ADD COLUMN new_price DECIMAL(10, 2);
- データを同期する: トリガーまたはバックグラウンドジョブを使用して、古い列から新しい列にデータをコピーします。これにより、移行中に両方の列が同じデータを保持することが保証されます。
-- 例 (PostgreSQL トリガー、簡略化) CREATE OR REPLACE FUNCTION copy_price_func() RETURNS TRIGGER AS $$ BEGIN NEW.new_price := NEW.old_price; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER copy_price_trigger BEFORE INSERT OR UPDATE ON products FOR EACH ROW EXECUTE FUNCTION copy_price_func(); -- 既存のデータもバックフィルする UPDATE products SET new_price = old_price;
- 新しいアプリケーションコードをデプロイする: アプリケーションを
new_price
列から読み書きするように更新します。古いコードは引き続きold_price
を使用します。 - 古い列を削除する: 新しいアプリケーションコードが完全にデプロイされ安定したら、古いコードパスが
old_price
に依存していないと確信できたら、安全に古い列を削除します。ALTER TABLE products DROP COLUMN old_price;
列のデータ型を変更する: 名前変更と同様に、データ型を直接変更すると、多くの場合、列を削除して再追加する必要があり、これは破壊的です。より安全なアプローチは次のとおりです。
- 希望するデータ型で新しい列を追加する:
ALTER TABLE events ADD COLUMN new_timestamp TIMESTAMP WITH TIME ZONE;
- データを同期する: 古い列から新しい列にデータをコピーして変換します。これには型キャストが含まれる場合があります。
UPDATE events SET new_timestamp = old_timestamp::TIMESTAMP WITH TIME ZONE;
- 新しいアプリケーションコードをデプロイする: アプリケーションは
new_timestamp
列の使用を開始します。 - 古い列を削除する。
列を削除するための原則
列を削除することは最も破壊的なスキーマ変更であり、最も慎重さを必要とします。主な戦略は、まず非推奨にすることです。
-
アプリケーションコードで非推奨にする: 列への書き込みを停止し、読み取り時にその値を無視するようにアプリケーションコードを変更します。しかし、列はデータベースに存在したままです。このバージョンをデプロイします。
-
使用状況を監視する: アプリケーションのどの部分や外部サービスも、非推奨になった列に依存していないことを確認します。可能であれば、データベース監視ツールを使用して列へのアクセスパターンを追跡します。
-
猶予期間: すべての古いアプリケーションバージョンが本番環境から削除され、忘れられた依存関係が特定されたことを確認するために、十分な猶予期間(数週間または数ヶ月)を設けます。
-
列を削除する: 列がもはや使用されていないと完全に確信したら、安全に削除できます。
ALTER TABLE orders DROP COLUMN old_deprecated_field;
機密データや重要なデータについては、列(または行)を物理的に削除するのではなく、「削除済み」フラグを設定するソフトデリートを検討することも賢明です。これは直接的な列削除の課題には対処しませんが、データ削除に対する慎重なアプローチを具体化しています。
実世界の考慮事項とツール
- データベース固有の機能: 最新のRDBMS(PostgreSQL、MySQL、SQL Server)は、さまざまなオンラインスキーママイグレーション機能を提供しています。PostgreSQL の
ALTER TABLE ADD COLUMN ... DEFAULT ... NOT NULL
は、テーブル全体を再書き込みせずに NULL 不可能な列を追加するのに非常に高速ですが、既存の行のUPDATE
フェーズ中にはロックが必要になります。MySQL 5.6 以降のALGORITHM=INSTANT
またはINPLACE
は、特定のALTER TABLE
操作をはるかに高速に実行できます。必ずデータベースのドキュメントを参照してください。 - プロキシツール: MySQL 用の Percona Toolkit の
pt-online-schema-change
や gh-ost(GitHub のオンラインスキーママイグレーションツール)のようなツールは、新しいテーブルを作成し、データをコピーし、変更を適用し、その後テーブルをスワップすることによって、ダウンタイムなしの変更を実現します。これらは大きなテーブルに非常に効果的です。 - フィーチャーフラグ: スキーマ変更を必要とする新しい機能を紹介する場合、フィーチャーフラグを使用すると、スキーマ変更のデプロイメントと新しいアプリケーションロジックのデプロイメントを分離できます。これにより、機能のロールアウトと問題発生時のロールバックに対する制御を強化できます。
- 自動テスト: スキーマ変更後の徹底的なテスト(統合テスト、場合によっては本番環境での A/B テストを含む)は不可欠です。
結論
継続的に成長するアプリケーションのためにデータベーススキーマを進化させるには、戦略的で慎重なアプローチが必要です。後方互換性のような概念を理解し、多段階デプロイメント戦略を採用し、データベース固有の機能または専門ツールの力を借りることで、サービス可用性を犠牲にすることなく列を追加、変更、削除することが可能です。アジャイルなスキーマ進化への道のりは、慎重な計画、段階的な変更、および厳格なテストの旅であり、アプリケーションアーキテクチャが変化の絶え間ない風に柔軟で回復力があることを保証します。