データモデルの列名・順序・型がいつの間にかズレていた——その事故を未然に防ぐのが、dbtのData Contracts(モデル契約)です。モデルに対して契約を有効化すると、コンパイル後のSQLで列と型が明示的に固定され、想定外の列混入や型ブレを早期に検知できます。
本稿では、契約の有効化手順、コンパイル後の具体挙動、インクリメンタルとの組み合わせ、テスト/DB制約との違い、そして試験で問われやすい論点を、公式ドキュメントに沿った安定挙動ベースで解説します。
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 + モデル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リストに依存するため、キャストが型を固定化します。
契約強制時のコンパイル像(簡略化した 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やフルリフレッシュの設計と組み合わせて対応します。
代表的な運用パターンは以下です。変更が大きい場合や型変更を伴う場合は、フルリフレッシュが最も安全です。
同じ“品質担保”でも、契約・テスト・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等) |
実装時に踏みがちな罠を避けるため、以下を出荷前チェックとして回しておくと安全です。
Analytics Engineer
問題 1
下流に提供するモデルの列名・順序・型を固定し、上流で列が増えても下流スキーマを崩さないようにしたい。さらに不足列や型不一致はモデル実行時に失敗させたい。最も適切なアプローチはどれか。
正解: A
契約の強制により、コンパイル後の外側SELECTで宣言済みの列と型のみを出力します。未宣言列は落ち、不足列や型不一致は実行時に失敗します。テストは内容検証でありスキーマ固定化ではありません。
契約はどの対象で使えるか。ソースやスナップショットにも適用できる?
契約はdbtの“モデル”に対して利用します。ソースは外部関係の宣言であり、契約の対象ではありません。スナップショットは履歴管理の仕組みで、原則として契約の主対象ではありません。
マルチアダプタ環境でdata_type指定はどうする?
data_typeはアダプタ固有です。環境が分かれる場合は、環境変数やJinjaの条件分岐、型エイリアス用のマクロを用意し、アダプタごとに適切な型名(INT64/STRING, NUMBER/VARCHAR 等)を出し分けます。
上流で新しい列が追加された場合どうなる?
契約を強制していれば、その列は出力に含まれません(外側SELECTで切り落とされます)。モデルの契約を更新して取り込みたい場合は、YAMLに列を追加してdata_typeを指定し、必要に応じてインクリメンタルのon_schema_changeやフルリフレッシュを検討します。
NicheeLab編集部
データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。
dbt Model の基礎: SQL で定義する変換の最小単位
Analytics Engineer 向けに、dbt Model の定義、マテリアライゼーション、依存関係、インクリメン...
dbt Analytics Engineer 試験ガイド: 出題範囲・配点・申込の実務視点
dbt Analytics Engineer 認定の出題範囲、配点の考え方、申込から受験までの流れを、公式ドキュメントの...
dbt Cloud と dbt Core の違いと選び方:Analytics Engineer 試験に効く要点
dbt Cloud と dbt Core の機能差を、実務と資格対策の両面から整理。スケジューリング、IDE、RBAC、...
dbt プロジェクト構造ガイド: models / seeds / macros の実務レイアウト
Analytics Engineer 向けに、dbt プロジェクトのディレクトリ構造と命名規約、dbt_project....
dbt_project.yml の読み方:主要設定と命名を最短で掴む
dbt_project.yml の必須キー、命名解決(database.schema.identifier)、設定優先度...