dbt

dbt Data Contracts実践ガイド: モデル契約の強制でスキーマを守る

2026-04-19
NicheeLab編集部

データモデルの列名・順序・型がいつの間にかズレていた——その事故を未然に防ぐのが、dbtのData Contracts(モデル契約)です。モデルに対して契約を有効化すると、コンパイル後のSQLで列と型が明示的に固定され、想定外の列混入や型ブレを早期に検知できます。

本稿では、契約の有効化手順、コンパイル後の具体挙動、インクリメンタルとの組み合わせ、テスト/DB制約との違い、そして試験で問われやすい論点を、公式ドキュメントに沿った安定挙動ベースで解説します。

Data Contractsとは何か、なぜ強制するのか

Data Contractsは、dbtモデルの出力スキーマ(列名・順序・データ型)を宣言し、それを実行時に強制する仕組みです。強制すると、コンパイル済みSQLが宣言済みの列だけを選択し、必要に応じて明示的に型キャストします。これにより、上流変更による“勝手に列が増えた/型が変わった”をモデルの境界で食い止められます。

Analytics Engineer試験の観点では、契約の有効化方法、カラム宣言の必須性、型指定のアダプタ依存性、インクリメンタル時のスキーマ変更対応(on_schema_changeやフルリフレッシュ)が頻出ポイントです。

  • 境界の明確化: モデル出力の“約束事”をコード化して破らせない
  • 失敗の前倒し: 期待しない列/欠落列/不一致型を実行時に検知
  • 影響の局所化: 上流の列追加があっても下流の契約は安定化

契約の有効化と宣言要件

契約はモデルごとに有効化します。YAMLまたはモデルSQLのconfigでcontract.enforcedをtrueにし、columnsで全列を宣言します。契約を強制する場合、各列のデータ型(data_type)をアダプタ(例: BigQuery, Snowflake, Postgres/Redshift)に合わせて指定するのが実務上の前提です。

列名の変更やエイリアスは、SQL側の出力列名とYAML側のcolumns.nameを一致させる必要があります。未宣言の列は出力に含まれず、宣言済みだがSQLに存在しない列は実行時エラーになります。

  • YAMLでcolumnsを全列宣言し、各列にdata_typeを指定する
  • SQL側の出力列名とYAML側のcolumns.nameを一致させる
  • select *は許容されるが、契約にない上流新規列は自動的に落ちる(無視される)点を理解する
  • 不足列は実行時エラー。早期検知に有効

契約の最小実装例(YAML + モデルSQL)

# schema.yml
models:
  - name: orders_mart
    config:
      contract:
        enforced: true
    columns:
      - name: order_id
        data_type: INT64        # 例: BigQuery
      - name: customer_id
        data_type: INT64
      - name: order_total
        data_type: NUMERIC
      - name: order_ts
        data_type: TIMESTAMP

---
-- models/orders_mart.sql
{{ config(materialized='table', contract={'enforced': true}) }}
with src as (
  select * from {{ ref('stg_orders') }}
)
select
  order_id,
  customer_id,
  order_total,
  order_ts
from src

コンパイル後の実際の挙動を理解する

契約を強制すると、dbtは内部CTEで元のクエリを包み、その外側で“宣言済みの列だけ”を“宣言した順序・型”で選択します。この外側SELECTで暗黙/明示のキャストが入り、未宣言列は切り落とされます。宣言済みだが内側に存在しない列はデータベース側で未定義列参照として失敗します。

アダプタ差分はありますが、共通要旨は“外側SELECTで列・順序・型を固定”です。ビューでも列型はSELECTリストに依存するため、キャストが型を固定化します。

  • 余剰列は外側SELECTでドロップされる
  • 不足列は実行時に“列が見つからない”として失敗
  • 型不一致は外側SELECTのキャストで是正される(キャスト不能なら失敗)

契約強制時のコンパイル像(簡略化した SQL)

WITH model__dbt_internal AS (
  -- 元のクエリ(上流変更の影響を受けうる)
  SELECT * FROM stg_orders
)
SELECT
  CAST(order_id    AS INT64)     AS order_id,
  CAST(customer_id AS INT64)     AS customer_id,
  CAST(order_total AS NUMERIC)   AS order_total,
  CAST(order_ts    AS TIMESTAMP) AS order_ts
FROM model__dbt_internal

インクリメンタルモデルとスキーマ変更の取り扱い

インクリメンタル素材でも契約は有効です。ただし“既存テーブルの物理スキーマ変更”は別問題です。契約は出力の列/型を論理的に固定しますが、実テーブルの列追加・型変更はon_schema_changeやフルリフレッシュの設計と組み合わせて対応します。

代表的な運用パターンは以下です。変更が大きい場合や型変更を伴う場合は、フルリフレッシュが最も安全です。

  • 新規列の追加: YAMLに列を追加しdata_typeを指定。インクリメンタルではon_schema_change=append_new_columns等(対応アダプタのみ)を検討。非対応や型制約が厳しい場合はフルリフレッシュ
  • 列の削除/リネーム: 自動ドロップは基本行わない。SQLとYAMLの同期調整後、必要に応じてフルリフレッシュ
  • 型変更: 既存データとの整合が崩れる可能性が高い。新列を追加→移行→旧列廃止、またはフルリフレッシュ
  • dbt buildの順序: 契約はrun/モデル作成時に効く。テストはその後に実行される

契約 vs テスト vs データベース制約の違い

同じ“品質担保”でも、契約・テスト・DB制約は役割が異なります。契約はスキーマ境界の固定化、テストはデータ特性の検証、DB制約はストレージレベルの一貫性担保です。使い分けを押さえておくと設計の意図が明確になります。

  • 契約は“出力カラムの約束事”を崩さない仕組み
  • テストは“内容(空/重複/参照整合)”を検証
  • DB制約は“ストレージの最終防衛線”。アダプタごとに実効性が異なる
項目目的/対象失敗タイミング代表的設定・例
Data Contracts出力スキーマ(列・順序・型)の固定モデル実行時(コンパイル後のSELECTで検知)config: contract.enforced=true + YAML columnsにdata_type
dbt テストNOT NULL/UNIQUE/関係性などデータ特性dbt test(またはdbt buildのテストフェーズ)tests: not_null, unique, relationships など
DB制約NOT NULL/PK/FK/CHECK等のストレージ整合DML/DDL時(アダプタの実装に依存)constraintsサポート有無はアダプタ依存(例: NOT NULL等)

落とし穴とチェックリスト

実装時に踏みがちな罠を避けるため、以下を出荷前チェックとして回しておくと安全です。

  • columnsに未宣言の列がないか。全列にdata_typeが入っているか
  • SQLの出力列名とYAMLのcolumns.nameが一致しているか
  • インクリメンタルで新規列追加時のon_schema_change方針が決まっているか
  • 型変更が必要な場合はフルリフレッシュ計画があるか
  • dbt compileとdbt run(またはdbt build)を本番相当環境で通せるか

問題で確認

Analytics Engineer

問題 1

下流に提供するモデルの列名・順序・型を固定し、上流で列が増えても下流スキーマを崩さないようにしたい。さらに不足列や型不一致はモデル実行時に失敗させたい。最も適切なアプローチはどれか。

  1. モデルにcontract.enforced=trueを設定し、YAMLで全列とdata_typeを宣言する
  2. not_nullやuniqueなどのテストだけを追加する
  3. データベースのインデックスを増やして実行計画を安定化させる
  4. モデルSQLで常にselect *を使い、上流での列追加を取り込む

正解: A

契約の強制により、コンパイル後の外側SELECTで宣言済みの列と型のみを出力します。未宣言列は落ち、不足列や型不一致は実行時に失敗します。テストは内容検証でありスキーマ固定化ではありません。

よくある質問

契約はどの対象で使えるか。ソースやスナップショットにも適用できる?

契約はdbtの“モデル”に対して利用します。ソースは外部関係の宣言であり、契約の対象ではありません。スナップショットは履歴管理の仕組みで、原則として契約の主対象ではありません。

マルチアダプタ環境でdata_type指定はどうする?

data_typeはアダプタ固有です。環境が分かれる場合は、環境変数やJinjaの条件分岐、型エイリアス用のマクロを用意し、アダプタごとに適切な型名(INT64/STRING, NUMBER/VARCHAR 等)を出し分けます。

上流で新しい列が追加された場合どうなる?

契約を強制していれば、その列は出力に含まれません(外側SELECTで切り落とされます)。モデルの契約を更新して取り込みたい場合は、YAMLに列を追加してdata_typeを指定し、必要に応じてインクリメンタルのon_schema_changeやフルリフレッシュを検討します。

この記事で学んだ内容を問題で確認しましょう

16,000問以上の問題で実力チェック

無料で問題を解いてみる
この記事の著者

NicheeLab編集部

データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。


関連記事
dbt

dbt Model の基礎: SQL で定義する変換の最小単位

Analytics Engineer 向けに、dbt Model の定義、マテリアライゼーション、依存関係、インクリメン...

dbt

dbt Analytics Engineer 試験ガイド: 出題範囲・配点・申込の実務視点

dbt Analytics Engineer 認定の出題範囲、配点の考え方、申込から受験までの流れを、公式ドキュメントの...

dbt

dbt Cloud と dbt Core の違いと選び方:Analytics Engineer 試験に効く要点

dbt Cloud と dbt Core の機能差を、実務と資格対策の両面から整理。スケジューリング、IDE、RBAC、...

dbt

dbt プロジェクト構造ガイド: models / seeds / macros の実務レイアウト

Analytics Engineer 向けに、dbt プロジェクトのディレクトリ構造と命名規約、dbt_project....

dbt

dbt_project.yml の読み方:主要設定と命名を最短で掴む

dbt_project.yml の必須キー、命名解決(database.schema.identifier)、設定優先度...

dbtの記事一覧 (100件)
© 2026 NicheeLab All rights reserved.