ソースシステム側が「ちょっとカラムを追加しただけ」の軽い気持ちでスキーマを変更した翌朝、データ基盤のパイプラインが赤く燃え、下流のBIダッシュボードが空白になる――この痛みを経験したデータエンジニアは決して少なくありません。スキーマ進化(Schema Evolution)の設計は、こうした「静かなAPI破壊」を防ぐための技術です。本記事では、後方互換性の考え方から、dbt contractsやApache Icebergのスキーマ進化機能まで、破壊的変更を未然に防ぐ実装パターンを解説します。

スキーマ進化とは何か――「カラム追加でパイプラインが壊れる」問題

スキーマ進化とは、データのスキーマ(テーブル定義)が時間とともに変化することに、パイプラインとストレージを追従させる設計の考え方です。ソースシステムの新機能追加、ビジネス要件の変化、バグ修正――あらゆる理由でスキーマは変わり続けます。問題は、ソースの1箇所の変更が、DWHとBI全体に波及することです。

特に危険なのが「SELECT *で受けているパイプライン」です。ソース側で順序が変わった、型が変わった、カラムが消えた――このいずれかが起きた瞬間、取り込みジョブは失敗するか、もっと悪い場合は「成功したように見えて値が入れ替わっている」という恐ろしい事象を引き起こします。スキーマ進化の設計とは、こうした変更を検知し、互換性を保ったまま進化させる仕組みのことです。データコントラクトとの併用で、より強固な防御が可能になります。

スキーマ変更の分類

スキーマ変更は「互換性」の観点で3種類に分類できます。後方互換(Backward Compatible)は、新スキーマで古いデータを読める変更、前方互換(Forward Compatible)は古いスキーマで新データを読める変更、そして両方満たさない破壊的変更(Breaking Change)です。この分類を押さえておけば、レビュー時に「これは受け入れていいか」の判断が機械的にできるようになります。

変更種別具体例下流影響対応必要度
後方互換(安全)nullableなカラム追加、enumの値追加ほぼなし
前方互換(条件付安全)optionalなフィールド削除古い読み手は無視
破壊的変更(危険)カラム削除、型変更、NOT NULL追加パイプライン全破壊
リネームカラム名の変更SELECT依存コードすべて破壊
【スキーマ変更影響フロー】

[スキーマ変更申請]
     |
     v
変更種別の判定
├── 後方互換 --> そのまま適用OK
├── 前方互換 --> 下流読み手の確認後に適用
└── 破壊的   --> バージョニング必須
                  |
                  v
              [v2エンドポイント作成]
                  |
                  v
              [移行期間(2〜4週間)]
                  |
                  v
              [v1廃止]

破壊的変更を防ぐ設計パターン

破壊的変更を防ぐ最も効果的なパターンは、スキーマを「契約(Contract)」として明示的に定義することです。dbt contractsを使えば、モデルの出力スキーマをYAMLで宣言的に定義し、ビルド時に型やNOT NULL制約をチェックできます。PR時点で破壊的変更を検知し、マージ前にブロックできるのが最大のメリットです。

# models/marts/_marts.yml
version: 2
models:
  - name: dim_customers
    config:
      contract:
        enforced: true
    columns:
      - name: customer_id
        data_type: string
        constraints:
          - type: not_null
          - type: primary_key
      - name: email
        data_type: string

ストレージ層では、Apache Icebergのスキーマ進化機能が強力です。Icebergは内部的にカラムをIDで管理しているため、カラム名のリネームや順序変更が安全に行えます。Hiveテーブルで悩ましかった「カラム追加で過去データが崩れる」問題が起きません。オープンテーブルフォーマットの比較記事も合わせてご覧ください。

-- Icebergのスキーマ進化SQL例
ALTER TABLE prod.db.orders ADD COLUMN promo_code STRING;
ALTER TABLE prod.db.orders RENAME COLUMN amount TO total_amount;
ALTER TABLE prod.db.orders DROP COLUMN legacy_flag;

スキーマ変更の運用プロセス

技術だけでなく、運用プロセスとしてスキーマ変更を管理することも重要です。変更申請→影響分析→レビュー→テスト→本番適用→モニタリングの6ステップを定義し、CI/CDパイプラインに組み込みます。

フェーズ作業内容担当ツール例
申請PR作成・変更理由の明記ソース側開発者GitHub/GitLab
影響分析リネージで下流依存を洗い出しデータエンジニアdbt docs、DataHub
レビュー互換性種別の判定データチームリードコードレビュー
テスト契約チェック・品質テストCIdbt contract、Great Expectations
本番適用段階的ロールアウトオペレーターAirflow、GitHub Actions
モニタリング鮮度・件数変動の監視SRE/オンコールElementary、Monte Carlo

特に「影響分析」フェーズは省略されがちですが、下流に50本のdbtモデルがぶら下がっているテーブルを破壊的に変更するリスクは甚大です。リネージツールで依存を可視化し、影響を受ける全モデルのオーナーに事前通知する仕組みを持ちましょう。

ツール別のスキーマ進化対応

DWHとテーブルフォーマットによってスキーマ進化への対応度は大きく異なります。新規構築であれば、Iceberg/Delta Lakeなどのオープンテーブルフォーマットを採用することで、破壊的変更のリスクを大幅に下げられます。

製品カラム追加リネーム型変更備考
Snowflake可(ALTER)一部のみ可型変更は安全な方向のみ
BigQueryREQUIREDからNULLABLEのみALTER TABLE DROP COLUMNは2022以降
Redshift制限的大規模テーブルでコスト増
Apache Iceberg可(安全)可(ID管理)可(昇格)カラムID管理で最強
Delta Lake一部可mergeSchemaオプション
Hive(標準)危険不可位置ベースで破損しやすい

ベストプラクティス5選

スキーマ進化の運用で実効性の高いベストプラクティスを5つ紹介します。第一に、SELECT *の禁止です。明示的にカラムを指定することで、未知のカラム追加で処理が予期せず変わるリスクを排除できます。dbt stagingモデルでも、カラムを全て書き出すのが理想です。

第二に、破壊的変更は必ずバージョニングすることです。既存テーブルをv1として残し、新しいスキーマをv2として作成、移行期間を設けてから旧バージョンを廃止します。第三に、契約の強制です。dbt contractsやsoda-coreを使い、CIで自動的にスキーマを検証します。第四に、リネージの可視化です。DataHub、OpenLineage、dbt docsなどで下流依存を常に把握できる状態にしておきます。

第五に、コミュニケーション設計です。スキーマ変更はテクニカルな問題であると同時に組織的な問題でもあります。ソース側の開発チームとデータチームで定期的にスキーマレビュー会を開催し、「これから変えたい」を事前に共有する場を作るだけで、破壊的変更の9割は防げます。dbt入門と合わせて、運用プロセスの設計に取り組んでください。

まとめ

スキーマ進化の設計は、データ基盤の長期安定運用の鍵です。後方互換・前方互換・破壊的変更の3分類を理解し、dbt contractsやIcebergなどのツールで自動検証を組み込むことで、「ある朝突然パイプラインが燃える」事態を防げます。技術と運用プロセスの両面から、スキーマ変更を「管理された変化」に変えていきましょう。データパイプライン設計全体の中で位置づけることも重要です。

よくある質問(FAQ)

Q. スキーマ進化で最も注意すべき変更は?

カラムの削除と型変更が最も危険な破壊的変更です。下流のすべてのパイプラインとダッシュボードに影響するため、事前の影響分析とバージョニングによる移行期間の確保が必須です。

Q. スキーマ変更をCIで検知できますか?

はい。dbt contractsやsoda-coreのスキーマチェックをCI/CDに組み込むことで、マージ前に破壊的変更を検知できます。PRレビュー時点で自動的に互換性が検証されるため、運用負荷を下げられます。

Q. Apache Icebergのスキーマ進化はどう機能しますか?

IcebergはカラムID管理によりカラム名の変更・追加・削除を安全に行えます。カラム名ではなく内部IDで追跡するため、リネーム時もデータの整合性が保たれ、過去データを読む際にも問題が発生しません。