学べること
- Contractによるスキーマ保証の実装方法
- 型安全性と破壊的変更の検出
- unit testsとの組み合わせによるデータ品質向上
- 実測検証結果とエラーハンドリング
はじめに
dbtのContract機能は、モデルのスキーマを保証し、破壊的変更を事前に検出します。本記事では、Contractの設定方法、unit testsとの組み合わせ、実際のエラー検出を検証し、データ品質向上のベストプラクティスを明らかにします。
検証環境:
- dbtバージョン: 1.11.5
- dbt-bigqueryバージョン: 1.11.0
- 検証日時: 2026-02-17
検証概要
検証日時: 2026-02-17 22:30-22:35 JST
dbtバージョン: 1.11.5
dbt-bigqueryバージョン: 1.11.0
BigQueryプロジェクト: sdp-sb-yada-29d2
データセット: dbt_sandbox
リージョン: asia-northeast1
参照元: 公式ドキュメント
実測検証結果
✅ Contract正常動作: 5行、4.01秒
❌ Contract違反エラー: Compilation Error(BigQuery実行前に検出)
✅ unit test + Contract: PASS、4.18秒
Contractsとは
dbt Contractsは、モデルの出力スキーマ(列名・データ型)をYAML定義と完全に一致させる機能です。
目的:
- スキーマ変更による下流クエリの破綻を防止
- データ型の暗黙的変換を検出
- データ品質の事前保証
目次
1. Contractsの基本
1.1 Contractsの仕組み
flowchart TD A[モデル実行] --> B{Contract有効?} B -->|No| C[通常実行] B -->|Yes| D[YAML定義読み込み] D --> E[モデルSQL実行] E --> F[出力スキーマ取得] F --> G{YAML定義と一致?} G -->|Yes| H[テーブル作成] G -->|No| I[エラー停止<br/>不一致詳細表示] C --> H
1.2 基本設定
# models/schema.yml
models:
- name: dim_customers
description: "顧客ディメンション"
config:
contract:
enforced: true # Contract有効化
columns:
- name: customer_id
data_type: int64
description: "顧客ID(主キー)"
- name: customer_name
data_type: string
description: "顧客名"
- name: email
data_type: string
description: "メールアドレス"
- name: created_at
data_type: timestamp
description: "作成日時"-- models/dim_customers.sql
{{
config(
materialized='table',
contract={'enforced': true}
)
}}
select
customer_id,
customer_name,
email,
created_at
from {{ ref('stg_customers') }}1.3 サポート範囲
✅ サポート対象
- Materialization: table, view, incremental
- 定義場所:
dbt_project.yml,properties.yml - モデル種類: SQLモデルのみ
❌ 非サポート
- Materialization: materialized_view, ephemeral
- モデル種類: Pythonモデル
- その他: sources, seeds, snapshots
- BigQuery特有: Recursive CTEs
2. Unit Testsとの組み合わせ
2.1 契約 + Unit Tests = 完全な型安全性
flowchart LR A[Contract] --> C[出力スキーマ保証] B[Unit Tests] --> D[ロジック保証] C --> E[型安全性] D --> E E --> F[下流クエリ<br/>破綻防止] E --> G[データ品質<br/>保証]
2.2 実践例: Contractとunit testsの併用
# models/schema.yml
models:
- name: fct_order_summary
description: "注文サマリーファクト"
config:
contract:
enforced: true # スキーマ保証
columns:
- name: order_date
data_type: date
description: "注文日"
- name: customer_id
data_type: int64
description: "顧客ID"
- name: order_count
data_type: int64
description: "注文件数"
- name: total_amount
data_type: numeric
description: "合計金額"
unit_tests:
- name: test_order_aggregation_with_contract
description: "注文集計ロジック検証(型安全性込み)"
model: fct_order_summary
given:
- input: ref('stg_orders')
format: sql
rows: |
select
cast('2026-02-17' as date) as order_date,
cast(1 as int64) as customer_id,
cast(100 as int64) as order_id,
cast(50.00 as numeric) as order_amount
expect:
format: sql
rows: |
select
cast('2026-02-17' as date) as order_date,
cast(1 as int64) as customer_id,
cast(1 as int64) as order_count,
cast(50.00 as numeric) as total_amount効果:
- Contract: YAMLで定義した型(date, int64, numeric)を強制
- Unit Test: ロジックの正確性(集計処理)を検証
- 型安全性: unit testでもCASTで明示的に型指定
2.3 データ型不一致の早期検出
問題シナリオ: 暗黙的な型変換
-- models/dim_customers.sql
select
customer_id, -- int64として定義
customer_name,
is_active, -- 実際はbool型
created_at
from {{ ref('stg_customers') }}# models/schema.yml(誤った定義)
models:
- name: dim_customers
config:
contract:
enforced: true
columns:
- name: customer_id
data_type: int64
- name: customer_name
data_type: string
- name: is_active
data_type: int64 # ❌ 実際はboolだがint64と定義
- name: created_at
data_type: timestampContractなしの場合:
✅ モデル実行成功
→ is_activeがbool→int64に暗黙変換される
→ 下流クエリで `WHERE is_active = true` がエラー
Contractありの場合:
❌ モデル実行失敗(契約違反)
→ エラーメッセージ:
Contract Error in model dim_customers
| Column | Contract Type | Actual Type |
|--------------|---------------|-------------|
| customer_id | int64 | int64 | ✓
| customer_name| string | string | ✓
| is_active | int64 | bool | ✗
| created_at | timestamp | timestamp | ✓
Fix: Update YAML definition or model SQL
2.4 Unit Testsでの型検証
unit_tests:
- name: test_data_type_enforcement
description: "データ型が正しく保持されることを検証"
model: dim_customers
given:
- input: ref('stg_customers')
format: sql
rows: |
select
cast(1 as int64) as customer_id,
cast('Alice' as string) as customer_name,
cast(true as bool) as is_active, -- bool型を明示
cast('2026-02-17 10:00:00' as timestamp) as created_at
expect:
format: sql
rows: |
select
cast(1 as int64) as customer_id,
cast('Alice' as string) as customer_name,
cast(true as bool) as is_active, -- bool型を期待
cast('2026-02-17 10:00:00' as timestamp) as created_at効果:
- unit testで型をCASTで明示
- Contractで出力スキーマを保証
- 型変換の問題を開発段階で発見
3. データ型の厳密な管理
3.1 データ型エイリアス
dbtは汎用型名(string, int, float)をプラットフォーム固有の型に変換します。
デフォルト動作(alias_types: true)
columns:
- name: customer_name
data_type: string # → BigQuery: STRING
- name: age
data_type: int # → BigQuery: INT64
- name: score
data_type: float # → BigQuery: FLOAT64エイリアス無効化
models:
- name: strict_schema_model
config:
contract:
enforced: true
alias_types: false # エイリアス無効化
columns:
- name: customer_name
data_type: STRING # BigQuery固有の型名を使用
- name: age
data_type: INT64
- name: score
data_type: FLOAT643.2 精度(Precision)とスケール(Scale)
NUMERICの精度管理
columns:
- name: price
data_type: numeric(10, 2) # 精度10、スケール2
# 例: 12345678.90(10桁、小数点以下2桁)
- name: tax_rate
data_type: numeric(5, 4) # 精度5、スケール4
# 例: 0.0825(5桁、小数点以下4桁)重要: スケールを指定しないと暗黙的な強制変換が発生する可能性あり
# ❌ Bad: スケール未指定
columns:
- name: amount
data_type: numeric
# ✅ Good: スケール明示
columns:
- name: amount
data_type: numeric(15, 2)3.3 VARCHAR長の柔軟な扱い
BigQueryでは、varchar(256) と varchar(257) の細かい違いは無視されます。
# YAML定義
columns:
- name: description
data_type: string(256)
# 実際のテーブル
CREATE TABLE ... (
description STRING(257) -- 1文字違うがOK
)4. 実践例と検証
4.1 完全な型安全性の例
# models/schema.yml
models:
- name: fct_sales_type_safe
description: "型安全な売上ファクト"
config:
contract:
enforced: true
materialized: incremental
unique_key: sale_id
columns:
- name: sale_id
data_type: int64
description: "売上ID"
- name: sale_date
data_type: date
description: "売上日"
- name: product_id
data_type: int64
description: "商品ID"
- name: quantity
data_type: int64
description: "数量"
- name: unit_price
data_type: numeric(10, 2)
description: "単価"
- name: total_amount
data_type: numeric(12, 2)
description: "合計金額"
- name: is_refunded
data_type: bool
description: "返金フラグ"
unit_tests:
- name: test_sales_calculation_type_safe
description: "売上計算の型安全性検証"
model: fct_sales_type_safe
given:
- input: ref('stg_sales')
format: sql
rows: |
select
cast(1 as int64) as sale_id,
cast('2026-02-17' as date) as sale_date,
cast(100 as int64) as product_id,
cast(5 as int64) as quantity,
cast(19.99 as numeric) as unit_price
expect:
format: sql
rows: |
select
cast(1 as int64) as sale_id,
cast('2026-02-17' as date) as sale_date,
cast(100 as int64) as product_id,
cast(5 as int64) as quantity,
cast(19.99 as numeric) as unit_price,
cast(99.95 as numeric) as total_amount, -- quantity * unit_price
cast(false as bool) as is_refunded-- models/fct_sales_type_safe.sql
{{
config(
materialized='incremental',
unique_key='sale_id',
contract={'enforced': true}
)
}}
select
sale_id,
sale_date,
product_id,
quantity,
unit_price,
quantity * unit_price as total_amount,
false as is_refunded
from {{ ref('stg_sales') }}
{% if is_incremental() %}
where sale_date > (select max(sale_date) from {{ this }})
{% endif %}4.2 Incrementalモデルでの注意点
Contractを使用するincrementalモデルでは、スキーマ変更時の動作を指定する必要があります。
models:
- name: fct_orders_incremental
config:
contract:
enforced: true
materialized: incremental
unique_key: order_id
on_schema_change: append_new_columns # または fail
columns:
- name: order_id
data_type: int64
- name: customer_id
data_type: int64
- name: order_amount
data_type: numeric(10, 2)
# 新しい列を追加する場合
- name: payment_method
data_type: string推奨設定:
| on_schema_change | 動作 | 推奨ケース |
|---|---|---|
append_new_columns | 新列を追加 | 開発環境 |
fail | エラーで停止 | 本番環境(慎重な変更) |
4.3 契約違反時のエラーメッセージ
# dbt run実行
$ dbt run --select fct_sales_type_safe
# エラー出力
Compilation Error in model fct_sales_type_safe (models/fct_sales_type_safe.sql)
Please ensure the name, data_type, and number of columns in your contract
match the columns in your model's definition.
| Column Name | Definition Type | Contract Type | Matches |
|--------------|-----------------|---------------|---------|
| sale_id | INT64 | INT64 | ✓ |
| sale_date | DATE | DATE | ✓ |
| product_id | INT64 | INT64 | ✓ |
| quantity | INT64 | INT64 | ✓ |
| unit_price | NUMERIC | NUMERIC(10,2) | ✓ |
| total_amount | FLOAT64 | NUMERIC(12,2) | ✗ |
| is_refunded | BOOL | BOOL | ✓ |
The contract of 'fct_sales_type_safe' failed.
Fix the SQL in fct_sales_type_safe.sql or adjust the contract in schema.yml問題: total_amount が FLOAT64 で計算されているが、Contractでは NUMERIC(12,2) を期待
修正:
-- ❌ Bad
select
quantity * unit_price as total_amount -- FLOAT64になる
-- ✅ Good
select
cast(quantity * unit_price as numeric) as total_amount4.4 実測検証結果(2026-02-17実施)
検証1: Contract違反エラーの実演
モデル定義: contract_test_model.sql
-- 意図的に型を間違えてエラーを発生させる
select
cast(customer_id as STRING) as customer_id, -- INT64で定義したのにSTRINGを返す
first_name,
last_name
from {{ ref('stg_customers') }}
limit 5Contract定義: _contract_test.yml
models:
- name: contract_test_model
description: "Contract違反エラー検証用モデル"
config:
contract:
enforced: true
columns:
- name: customer_id
data_type: int64 # INT64を期待(SQLではSTRINGを返す)
- name: first_name
data_type: string
- name: last_name
data_type: string実行コマンド:
dbt run --select contract_test_model --profiles-dir . --target sandbox実行結果(エラー):
22:34:00 1 of 1 START sql table model dbt_sandbox.contract_test_model ....... [RUN]
22:34:02 1 of 1 ERROR creating sql table model dbt_sandbox.contract_test_model [ERROR in 1.67s]
Compilation Error in model contract_test_model (models/contract_test_model.sql)
This model has an enforced contract that failed.
Please ensure the name, data_type, and number of columns in your contract match the columns in your model's definition.
| column_name | definition_type | contract_type | mismatch_reason |
| ----------- | --------------- | ------------- | ------------------ |
| customer_id | STRING | INT64 | data type mismatch |
> in macro assert_columns_equivalent (macros/relations/column/columns_spec_ddl.sql)
> called by macro default__get_assert_columns_equivalent (macros/relations/column/columns_spec_ddl.sql)
> called by macro get_assert_columns_equivalent (macros/relations/column/columns_spec_ddl.sql)
> called by macro bigquery__create_table_as (macros/adapters.sql)
> called by macro create_table_as (macros/relations/table/create.sql)
> called by macro statement (macros/etc/statement.sql)
> called by macro materialization_table_bigquery (macros/materializations/table.sql)
> called by model contract_test_model (models/contract_test_model.sql)
Done. PASS=0 WARN=0 ERROR=1 SKIP=0 NO-OP=0 TOTAL=1
📋 詳細な実行ログ(クリックで展開)
22:33:55 Running with dbt=1.11.5
22:33:58 Registered adapter: bigquery=1.11.0
22:33:59 WARNING: The microbatch model 'incr_microbatch_demo' has no 'ref' or 'source' input with an 'event_time' configuration.
22:33:59 Found 27 models, 3 seeds, 31 data tests, 539 macros, 9 unit tests
22:33:59
22:33:59 Concurrency: 24 threads (target='sandbox')
22:33:59
22:34:00 1 of 1 START sql table model dbt_sandbox.contract_test_model ................... [RUN]
22:34:02 1 of 1 ERROR creating sql table model dbt_sandbox.contract_test_model .......... [ERROR in 1.67s]
22:34:02
22:34:02 Finished running 1 table model in 0 hours 0 minutes and 3.30 seconds (3.30s).
22:34:02
22:34:02 Completed with 1 error, 0 partial successes, and 0 warnings:
22:34:02
22:34:02 Failure in model contract_test_model (models/contract_test_model.sql)
22:34:02 Compilation Error in model contract_test_model (models/contract_test_model.sql)
This model has an enforced contract that failed.
Please ensure the name, data_type, and number of columns in your contract match the columns in your model's definition.
| column_name | definition_type | contract_type | mismatch_reason |
| ----------- | --------------- | ------------- | ------------------ |
| customer_id | STRING | INT64 | data type mismatch |
> in macro assert_columns_equivalent (macros/relations/column/columns_spec_ddl.sql)
> called by macro default__get_assert_columns_equivalent (macros/relations/column/columns_spec_ddl.sql)
> called by macro get_assert_columns_equivalent (macros/relations/column/columns_spec_ddl.sql)
> called by macro bigquery__create_table_as (macros/adapters.sql)
> called by macro create_table_as (macros/relations/table/create.sql)
> called by macro statement (macros/etc/statement.sql)
> called by macro materialization_table_bigquery (macros/materializations/table.sql)
> called by model contract_test_model (models/contract_test_model.sql)
22:34:02
22:34:02 compiled code at target/compiled/jaffle_shop/models/contract_test_model.sql
22:34:02
22:34:02 Done. PASS=0 WARN=0 ERROR=1 SKIP=0 NO-OP=0 TOTAL=1
重要な発見:
- ✅ Compilation Errorとして検出 = BigQueryにクエリを送信する前にdbtがエラーを検出
- ✅ BigQueryコストがかからない = 実行前にローカルでチェック
- ✅ 詳細なエラーメッセージ = どの列の型が不一致かを表形式で表示
- ✅ pre-commit/CIで効率的にチェック可能 = 実行コストなしで型安全性を保証
検証2: Contract正常動作の確認
モデル定義: contract_valid_model.sql
-- 型が正しく一致するケース
select
customer_id, -- INT64(契約と一致)
first_name, -- STRING(契約と一致)
last_name -- STRING(契約と一致)
from {{ ref('stg_customers') }}
limit 5Contract定義:
models:
- name: contract_valid_model
description: "Contract正常動作検証用モデル"
config:
contract:
enforced: true
columns:
- name: customer_id
data_type: int64
- name: first_name
data_type: string
- name: last_name
data_type: string実行コマンド:
dbt run --select contract_valid_model --profiles-dir . --target sandbox実行結果(成功):
22:34:32 1 of 1 START sql table model dbt_sandbox.contract_valid_model ...... [RUN]
22:34:36 1 of 1 OK created sql table model dbt_sandbox.contract_valid_model . [CREATE TABLE (5.0 rows, 1.9 KiB processed) in 4.01s]
Finished running 1 table model in 0 hours 0 minutes and 5.61 seconds (5.61s).
Completed successfully
Done. PASS=1 WARN=0 ERROR=0 SKIP=0 NO-OP=0 TOTAL=1
📋 詳細な実行ログ(クリックで展開)
22:34:27 Running with dbt=1.11.5
22:34:30 Registered adapter: bigquery=1.11.0
22:34:30 WARNING: The microbatch model 'incr_microbatch_demo' has no 'ref' or 'source' input with an 'event_time' configuration.
22:34:30 Found 28 models, 3 seeds, 31 data tests, 539 macros, 9 unit tests
22:34:30
22:34:30 Concurrency: 24 threads (target='sandbox')
22:34:30
22:34:32 1 of 1 START sql table model dbt_sandbox.contract_valid_model .................. [RUN]
22:34:36 1 of 1 OK created sql table model dbt_sandbox.contract_valid_model ............. [CREATE TABLE (5.0 rows, 1.9 KiB processed) in 4.01s]
22:34:36
22:34:36 Finished running 1 table model in 0 hours 0 minutes and 5.61 seconds (5.61s).
22:34:36
22:34:36 Completed successfully
22:34:36
22:34:36 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 NO-OP=0 TOTAL=1
実測データ:
- ✅ 実行成功: 5行作成
- ✅ 実行時間: 4.01秒
- ✅ データ処理量: 1.9 KiB
- ✅ Contract検証: 型が全て一致してパス
検証3: unit test + Contract組み合わせ
unit test定義: _contract_unit_test.yml
unit_tests:
- name: test_contract_with_unit_test
description: "unit testとContractの組み合わせ検証"
model: contract_valid_model
given:
- input: ref('stg_customers')
rows:
- { customer_id: 1, first_name: "Alice", last_name: "Smith" }
- { customer_id: 2, first_name: "Bob", last_name: "Jones" }
expect:
rows:
- { customer_id: 1, first_name: "Alice", last_name: "Smith" }
- { customer_id: 2, first_name: "Bob", last_name: "Jones" }実行コマンド:
dbt test --select test_name:test_contract_with_unit_test --profiles-dir . --target sandbox実行結果(成功):
22:35:20 1 of 1 START unit_test contract_valid_model::test_contract_with_unit_test ... [RUN]
22:35:24 1 of 1 PASS contract_valid_model::test_contract_with_unit_test .............. [PASS in 4.18s]
Finished running 1 unit test in 0 hours 0 minutes and 5.35 seconds (5.35s).
Completed successfully
Done. PASS=1 WARN=0 ERROR=0 SKIP=0 NO-OP=0 TOTAL=1
📋 詳細な実行ログ(クリックで展開)
22:35:15 Running with dbt=1.11.5
22:35:19 Registered adapter: bigquery=1.11.0
22:35:19 WARNING: The microbatch model 'incr_microbatch_demo' has no 'ref' or 'source' input with an 'event_time' configuration.
22:35:19 Found 28 models, 3 seeds, 31 data tests, 539 macros, 10 unit tests
22:35:19
22:35:19 Concurrency: 24 threads (target='sandbox')
22:35:19
22:35:20 1 of 1 START unit_test contract_valid_model::test_contract_with_unit_test ...... [RUN]
22:35:24 1 of 1 PASS contract_valid_model::test_contract_with_unit_test ................. [PASS in 4.18s]
22:35:24
22:35:24 Finished running 1 unit test in 0 hours 0 minutes and 5.35 seconds (5.35s).
22:35:24
22:35:24 Completed successfully
22:35:24
22:35:24 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 NO-OP=0 TOTAL=1
実測データ:
- ✅ unit test成功: PASS
- ✅ 実行時間: 4.18秒
- ✅ Contract検証: unit test実行時もContract enforcedが有効
効果の確認:
| 検証項目 | 結果 | 実行時間 | コスト |
|---|---|---|---|
| Contract違反エラー | ❌ Compilation Error | 1.67s | 0円(実行前検出) |
| Contract正常動作 | ✅ CREATE TABLE | 4.01s | ~0.001円 |
| unit test + Contract | ✅ PASS | 4.18s | ~0.001円 |
検証まとめ: Contractのチェックタイミング
flowchart TD A[dbt run/test実行] --> B[dbt compile] B --> C{Contract enforced?} C -->|No| D[BigQuery実行] C -->|Yes| E[Contract検証<br/>ローカル] E --> F{型一致?} F -->|No| G[❌ Compilation Error<br/>BigQuery実行なし<br/>コスト: 0円] F -->|Yes| H[BigQuery実行] H --> I[✅ 成功]
重要な学び:
- Contract違反は Compilation Error = BigQuery実行前に検出される
- BigQueryコストがかからない = ローカルで型チェックが完結
- pre-commit/CIでの効率的なチェック = 実行コストなしで型安全性を保証可能
- unit testとの併用で完全な品質保証 = 型(Contract) + ロジック(unit test)
5. ベストプラクティス
5.1 Contractチェックの運用戦略(pre-commit/CI)
どの段階でContractをチェックすべきか
flowchart LR A[ローカル開発] --> B[pre-commit] B --> C[CI: PR作成時] C --> D[CI: mainマージ前] D --> E[本番デプロイ] A --> A1[dbt run/compile<br/>手動実行] B --> B1[Contract検証<br/>コンパイルのみ] C --> C1[Contract + unit tests<br/>変更モデル] D --> D1[全Contract + 全tests<br/>完全検証] E --> E1[本番実行<br/>Contract enforced]
推奨設定一覧
| チェックポイント | 実行内容 | 目的 | 実行時間目安 | BigQueryコスト |
|---|---|---|---|---|
| ローカル開発 | dbt run --select <model> | 即座のフィードバック | 5-10秒/モデル | 小(開発環境) |
| pre-commit | dbt compile | SQL構文+Contract検証 | 3-5秒 | 0円(コンパイルのみ) |
| CI: PR作成 | dbt test --select state:modified+ | 変更されたモデルのみ | 30秒-2分 | 小(必要最小限) |
| CI: mainマージ前 | dbt test --select test_type:unit + dbt run --select state:modified+ | 全unit tests + 変更モデル実行 | 2-5分 | 中(unit testsは小データ) |
| 定期実行(毎日) | dbt test + dbt run | 全tests + 全モデル | 10-30分 | 大(本番規模データ) |
pre-commit設定例(Contract検証)
目的: コミット前にSQL構文とContract違反を検出(BigQueryコスト0円)
ファイル: .pre-commit-config.yaml
repos:
- repo: local
hooks:
# dbt compile check(Contractチェック含む)
- id: dbt-compile
name: dbt compile + Contract validation
entry: bash -c 'dbt compile --profiles-dir . --target dev'
language: system
pass_filenames: false
files: 'models/.*\.(sql|yml|yaml)$'
stages: [commit]
verbose: true
# SQLフォーマットチェック(オプション)
- id: sqlfluff-lint
name: sqlfluff lint
entry: sqlfluff lint
language: system
files: 'models/.*\.sql$'
pass_filenames: true実行例:
# pre-commitフックのインストール
pre-commit install
# 手動実行
pre-commit run --all-files
# 実行結果
dbt compile + Contract validation......................................Passed
- Duration: 3.2s
- Cost: 0円(BigQueryアクセスなし)効果:
- ✅ Contract違反をコミット前に検出
- ✅ BigQueryコスト0円(コンパイルのみ)
- ✅ 実行時間: 3-5秒(高速)
- ✅ チーム全体での品質保証
CI/CD設定例(GitHub Actions)
目的: PRマージ前にContract + unit testsで完全検証
ファイル: .github/workflows/dbt_pr_check.yml
name: dbt PR Check
on:
pull_request:
paths:
- "models/**"
- "macros/**"
- "dbt_project.yml"
jobs:
dbt-contract-unit-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install dbt
run: pip install dbt-bigquery
- name: Authenticate to BigQuery
uses: google-github-actions/auth@v1
with:
# GitHub環境変数から認証情報を取得
credentials_json: ${{ env.GCP_SA_KEY }}
# ステップ1: dbt compile(Contract検証)
- name: dbt compile (Contract validation)
run: |
dbt compile --profiles-dir . --target ci
continue-on-error: false # Contract違反で即座に失敗
# ステップ2: unit tests実行
- name: dbt unit tests
run: |
dbt test --select test_type:unit --profiles-dir . --target ci
# ステップ3: 変更されたモデルのみ実行(オプション)
- name: dbt run (changed models only)
run: |
dbt run --select state:modified+ --profiles-dir . --target ci --state ./prod_manifest/
- name: Upload logs
if: always()
uses: actions/upload-artifact@v3
with:
name: dbt-logs
path: logs/実行フロー:
- dbt compile → Contract違反で即座に失敗(コスト0円)
- unit tests → ロジック検証(コスト小)
- dbt run(変更モデル) → 実際のBigQuery実行(コスト中)
運用戦略の推奨構成
パターンA: コスト重視(推奨)
| チェックポイント | 実行内容 | 理由 |
|---|---|---|
| pre-commit | dbt compile のみ | コスト0円、高速 |
| CI: PR作成 | dbt compile + unit tests | 軽量、必要最小限 |
| CI: mainマージ前 | 全tests + 変更モデル実行 | 完全検証 |
メリット:
- ✅ BigQueryコストを最小化
- ✅ 高速フィードバック(pre-commit: 3秒)
- ✅ Contract違反を早期発見
デメリット:
- ⚠️ 実データでの検証は後回し
パターンB: 品質重視
| チェックポイント | 実行内容 | 理由 |
|---|---|---|
| pre-commit | dbt compile + unit tests | 完全検証 |
| CI: PR作成 | 全tests + 変更モデル実行 | 実データでも検証 |
| CI: mainマージ前 | 全tests + 全モデル実行 | 完全な品質保証 |
メリット:
- ✅ 最高の品質保証
- ✅ 実データでの検証も早期に実施
デメリット:
- ⚠️ BigQueryコスト増加
- ⚠️ 実行時間長い(pre-commit: 10-30秒)
パターンC: ハイブリッド(バランス型・推奨)
flowchart TD A[ローカル開発] --> B[dbt run 手動実行] B --> C{Contract違反?} C -->|Yes| D[修正] C -->|No| E[Git commit] E --> F[pre-commit: dbt compile] F --> G{構文/Contract OK?} G -->|No| H[コミット失敗] G -->|Yes| I[PR作成] I --> J[CI: dbt compile + unit tests] J --> K{成功?} K -->|No| L[PR修正要求] K -->|Yes| M[レビュー待ち] M --> N[CI: 全tests + 変更モデル実行] N --> O{成功?} O -->|No| P[マージ不可] O -->|Yes| Q[mainマージ] Q --> R[本番デプロイ]
特徴:
- pre-commit: コンパイルのみ(高速、コスト0円)
- CI PR作成: compile + unit tests(軽量)
- CI mainマージ前: 完全検証(品質保証)
推奨理由:
- ✅ 開発速度を維持: pre-commitは高速(3秒)
- ✅ コストを抑制: 早期段階はBigQueryアクセス最小限
- ✅ 品質を保証: mainマージ前は完全検証
- ✅ 段階的チェック: 問題を早く、安く発見
5.2 Contract + Unit Tests統合戦略
flowchart TD A[新規モデル作成] --> B[1. スキーマ定義<br/>schema.yml] B --> C[2. Contract有効化<br/>enforced: true] C --> D[3. 型を明示<br/>data_type指定] D --> E[4. Unit Test作成<br/>型をCASTで明示] E --> F[5. dbt run実行<br/>Contract検証] F --> G{契約違反?} G -->|Yes| H[SQLまたはYAML修正] G -->|No| I[6. Unit Test実行<br/>ロジック検証] I --> J{テスト成功?} J -->|Yes| K[完成] J -->|No| L[ロジック修正] H --> F L --> I
5.2 型定義のガイドライン
数値型の選択
| 用途 | 推奨型 | 理由 |
|---|---|---|
| ID(主キー) | int64 | 整数、範囲広い |
| カウント | int64 | 整数、負数なし |
| 金額 | numeric(15, 2) | 精度重視 |
| 割合・率 | numeric(5, 4) | 小数点以下精度 |
| 科学計算 | float64 | 範囲広い、精度は低い |
文字列型の選択
| 用途 | 推奨型 | 理由 |
|---|---|---|
| 名前・タイトル | string | 可変長 |
| 固定長コード | string(10) | 長さ制限明示 |
| 大量テキスト | string | 制限なし |
日付・時刻型の選択
| 用途 | 推奨型 | 理由 |
|---|---|---|
| 日付のみ | date | タイムゾーン不要 |
| 日時(UTC) | timestamp | タイムゾーン対応 |
| 日時(ローカル) | datetime | タイムゾーンなし |
5.3 契約記述のテンプレート
# テンプレート: Factテーブル
models:
- name: fct_<domain>
description: "<ドメイン>ファクトテーブル"
config:
contract:
enforced: true
materialized: incremental
unique_key: <primary_key>
on_schema_change: fail # 本番は慎重に
columns:
# 主キー
- name: <primary_key>
data_type: int64
description: "主キー"
# 日付・時刻
- name: event_date
data_type: date
description: "イベント日"
- name: created_at
data_type: timestamp
description: "作成日時"
# 外部キー
- name: customer_id
data_type: int64
description: "顧客ID(外部キー)"
# メトリクス
- name: amount
data_type: numeric(15, 2)
description: "金額"
- name: quantity
data_type: int64
description: "数量"
# フラグ
- name: is_active
data_type: bool
description: "有効フラグ"
unit_tests:
- name: test_<domain>_logic
description: "ロジック検証"
model: fct_<domain>
given:
- input: ref('stg_<source>')
format: sql
rows: |
select
cast(1 as int64) as <primary_key>,
cast('2026-02-17' as date) as event_date,
cast(100.00 as numeric) as amount
expect:
format: sql
rows: |
select
cast(1 as int64) as <primary_key>,
cast('2026-02-17' as date) as event_date,
cast(100.00 as numeric) as amount,
cast(true as bool) as is_active6. トラブルシューティング
問題1: 型不一致エラー
症状:
Contract Error: Column 'amount' has type FLOAT64 but contract expects NUMERIC
原因: 計算結果が予期しない型になる
解決策:
-- ❌ Bad
select quantity * price as amount
-- ✅ Good
select cast(quantity * price as numeric) as amount問題2: 列の順序不一致
症状:
Contract Error: Column order mismatch
原因: YAML定義とSELECT文の列順序が異なる
解決策:
# YAML定義順
columns:
- name: id
- name: name
- name: created_at-- ✅ 同じ順序でSELECT
select
id,
name,
created_at
from source問題3: Incremental + Contract のスキーマ変更
症状:
on_schema_change config must be set when using contracts with incremental models
解決策:
config:
contract:
enforced: true
materialized: incremental
on_schema_change: append_new_columns # 必須問題4: Unit TestでのNULL処理
症状: Unit testでNULLが期待通りに扱われない
解決策:
expect:
format: sql
rows: |
select
cast(1 as int64) as id,
cast(null as string) as optional_field -- NULLを明示的にCAST7. Contractsが検証できる項目の完全リスト(2026-02-18 追加検証)
7.1 検証項目の実測結果
検証日時: 2026-02-18 15:20-15:30 JST
| # | 検証項目 | 検証する/しない | 実行結果 | エラーメッセージ |
|---|---|---|---|---|
| 1 | 列名 | ✅ 検証する | FAIL | missing in definition / missing in contract |
| 2 | 列数 | ✅ 検証する | FAIL | missing in definition |
| 3 | データ型 | ✅ 検証する | FAIL | data type mismatch |
| 4 | 列の順序 | ❌ 検証しない | PASS | dbtが自動調整 |
重要な発見:
- Contractsはデータ型だけでなく、列名と列数も検証する
- 列の順序は検証されない(dbtがcontractの順序に自動調整)
7.2 列の順序は検証されない(実測確認)
テストモデル
-- models/contract_tests/contract_column_order.sql
select
customer_id, -- 1列目
first_name, -- 2列目
last_name -- 3列目
from {{ ref('stg_customers') }}Contract定義(意図的に順序を変える)
models:
- name: contract_column_order
config:
contract:
enforced: true
columns:
# YAML定義の順序: first_name, customer_id, last_name
- name: first_name # 1列目(SQLでは2列目)
data_type: string
- name: customer_id # 2列目(SQLでは1列目)
data_type: int64
- name: last_name # 3列目(SQLでも3列目)
data_type: string実行結果
$ dbt run --select contract_column_order --profiles-dir .
✅ 1 of 1 OK created sql table model dbt_jaffle_shop.contract_column_order
[CREATE TABLE (5.0 rows, 1.9 KiB processed) in 4.24s]
Done. PASS=1 WARN=0 ERROR=0 SKIP=0 NO-OP=0 TOTAL=1結果:
- ✅ 列の順序が異なってもPASS
- dbtがcontractの順序(first_name, customer_id, last_name)に自動調整
- BigQueryのテーブルは contract 定義の順序で作成される
公式ドキュメントの記載:
The preflight check is agnostic to the order of columns specified in your model, and dbt will order the columns per the contract instead of your dbt model.
出典: Model contracts | dbt Developer Hub
7.3 列名の検証
テストモデル(列名を変更)
-- models/contract_tests/contract_column_name.sql
select
customer_id,
first_name as given_name, -- ← 列名を変更
last_name
from {{ ref('stg_customers') }}実行結果(エラー)
$ dbt run --select contract_column_name --profiles-dir .
❌ Compilation Error in model contract_column_name
| column_name | definition_type | contract_type | mismatch_reason |
| ----------- | --------------- | ------------- | --------------------- |
| first_name | | STRING | missing in definition |
| given_name | STRING | | missing in contract |
Done. PASS=0 WARN=0 ERROR=1 SKIP=0 NO-OP=0 TOTAL=1結果:
- ✅ 列名の不一致を検出
- ✅ 2つのエラーを同時に表示
- ✅ BigQueryコスト: 0円(実行前に検出)
7.4 列数の検証
テストモデル(列が不足)
-- models/contract_tests/contract_column_count.sql
select
customer_id,
first_name
-- last_name を省略
from {{ ref('stg_customers') }}Contract定義(3列を期待)
columns:
- name: customer_id
data_type: int64
- name: first_name
data_type: string
- name: last_name # ← SQLに存在しない
data_type: string実行結果(エラー)
$ dbt run --select contract_column_count --profiles-dir .
❌ Compilation Error in model contract_column_count
| column_name | definition_type | contract_type | mismatch_reason |
| ----------- | --------------- | ------------- | --------------------- |
| last_name | | STRING | missing in definition |
Done. PASS=0 WARN=0 ERROR=1 SKIP=0 NO-OP=0 TOTAL=1結果:
- ✅ 列数の不一致を検出
- ✅
missing in definitionで不足している列を明示 - ✅ BigQueryコスト: 0円
7.5 Constraints(別機能)との違い
Constraints付きモデル
models:
- name: contract_with_constraints
config:
contract:
enforced: true
columns:
- name: customer_id
data_type: int64
constraints:
- type: not_null # 実行時に検証
- type: primary_key # BigQueryでは未enforced実行結果
$ dbt run --select contract_with_constraints --profiles-dir .
⚠️ WARNING: The constraint type primary_key is not enforced by bigquery.
✅ 1 of 1 OK created sql table model [CREATE TABLE (5.0 rows) in 4.09s]
Done. PASS=1 WARN=0 ERROR=0 SKIP=0 NO-OP=0 TOTAL=1Contracts vs Constraints:
| 項目 | Contracts | Constraints |
|---|---|---|
| 検証タイミング | コンパイル時 | 実行時(データ挿入時) |
| 検証内容 | スキーマ構造(列名・型・数) | データ値(not null, primary key) |
| BigQueryコスト | 0円(実行前) | 発生(実行される) |
| BigQueryサポート | - | not_null のみenforced |
| primary_key | 検証しない | DDLに含まれるが未enforced |
| check | 検証しない | サポートなし |
重要:
- Contractsは「スキーマの型安全性」を保証
- Constraintsは「データの品質」を保証
- Contractsはコンパイル時に検証(BigQueryコスト: 0円)
- Constraintsは実行時に検証(BigQueryコスト: 発生)
参考:
8. Contract有効化モデルの選択・除外コマンド
8.1 タグで選択
# contract_testタグが付いたモデルを一覧表示
$ dbt ls --select tag:contract_test --profiles-dir .
jaffle_shop.contract_tests.contract_column_count
jaffle_shop.contract_tests.contract_column_name
jaffle_shop.contract_tests.contract_column_order
jaffle_shop.contract_tests.contract_valid
jaffle_shop.contract_tests.contract_with_constraints8.2 有効なcontractモデルのみ実行
# contract_validタグが付いたモデルのみ実行(失敗するモデルを除外)
$ dbt run --select tag:contract_valid --profiles-dir .
✅ 1 of 2 OK created sql table model dbt_jaffle_shop.contract_valid
[CREATE TABLE (5.0 rows) in 4.01s]
✅ 2 of 2 OK created sql table model dbt_jaffle_shop.contract_with_constraints
[CREATE TABLE (5.0 rows) in 3.95s]
Done. PASS=2 WARN=0 ERROR=0 SKIP=0 NO-OP=0 TOTAL=28.3 特定モデルを除外して実行
# contract_testタグのモデルから、失敗するモデルを除外
$ dbt run \
--select tag:contract_test \
--exclude 'contract_column_count contract_column_name' \
--profiles-dir .
✅ 1 of 3 OK created sql table model dbt_jaffle_shop.contract_column_order
[CREATE TABLE (5.0 rows) in 4.23s]
✅ 2 of 3 OK created sql table model dbt_jaffle_shop.contract_valid
[CREATE TABLE (5.0 rows) in 4.76s]
✅ 3 of 3 OK created sql table model dbt_jaffle_shop.contract_with_constraints
[CREATE TABLE (5.0 rows) in 3.65s]
Done. PASS=3 WARN=0 ERROR=0 SKIP=0 NO-OP=0 TOTAL=38.4 使えるセレクター一覧
| セレクター | 説明 | 例 |
|---|---|---|
--select tag:X | タグで選択 | --select tag:contract_test |
--select model_name | モデル名で選択 | --select contract_valid |
--select config.X:Y | Config値で選択 | --select config.materialized:incremental |
--exclude model_name | モデルを除外 | --exclude contract_column_count |
--select 'X Y Z' | 複数選択(スペース区切り) | --select 'contract_valid orders' |
--exclude 'X Y Z' | 複数除外(スペース区切り) | --exclude 'model_a model_b' |
--select tag:X --exclude Y | 組み合わせ | --select tag:contract_test --exclude contract_column_count |
--select model+ | モデルと下流を選択 | --select stg_customers+ |
--select +model | モデルと上流を選択 | --select +fct_orders |
--select state:modified+ | 変更されたモデルと下流 | CI/CDで使用 |
参考: Node selector methods | dbt Developer Hub
8.5 Config Selectorでcontract有効化モデルを選択(高度な方法)
重要: dbt 1.8以降では、config値で直接モデルを選択できます。
# Contract enforced が true のモデルを全て選択
# 注意: この方法は理論上可能だが、実際にはタグを使う方が推奨
dbt run --select "config.contract.enforced:true" --profiles-dir .
# Incrementalモデルを選択
dbt run --select "config.materialized:incremental" --profiles-dir .
# Auditスキーマのモデルを選択
dbt run --select "config.schema:audit" --profiles-dir .実用性の評価:
- ✅ タグ不要: YAMLにタグを追加しなくても選択可能
- ⚠️ 可読性:
tag:contract_testの方が意図が明確 - ⚠️ 推奨: 実務ではタグを使う方が推奨(明示的で管理しやすい)
参考: Node selector methods | dbt Developer Hub
8.6 Graph Operators(グラフ演算子)でcontractモデルの依存関係を選択
# stg_customersとその下流モデルを全て実行
dbt run --select "stg_customers+" --profiles-dir .
# fct_ordersとその上流モデルを全て実行
dbt run --select "+fct_orders" --profiles-dir .
# contract_validとその1階層下流のみ実行
dbt run --select "contract_valid+1" --profiles-dir .
# contract_validとその上流2階層を実行
dbt run --select "2+contract_valid" --profiles-dir .
# contract_testタグのモデルとその下流を全て実行
dbt run --select "tag:contract_test+" --profiles-dir .グラフ演算子の優先順位:
- Selector methods (tag:, config:, model名)
- Graph operators (+, @)
- Set operators (union, intersection, exclusion)
参考: Graph operators | dbt Developer Hub
8.7 YAML Selectorsで再利用可能な選択パターンを定義
ファイル: selectors.yml(プロジェクトルートに配置)
selectors:
# Contract有効化モデルのみ実行
- name: contract_models
description: "Contract enforced が有効なモデル"
definition:
union:
- tag:contract_valid
- tag:contract_enforced
# CI/CDで使用: 変更されたcontractモデルとその下流
- name: ci_contract_check
description: "変更されたcontractモデルと下流を検証"
definition:
union:
- intersection:
- state:modified+
- tag:contract_enforced
# Incrementalモデルでcontract有効化されているもの
- name: incremental_with_contract
description: "Contract有効化されたIncrementalモデル"
definition:
intersection:
- config.materialized:incremental
- tag:contract_enforced使用方法:
# YAML Selectorを使用
dbt run --selector contract_models --profiles-dir .
# CI/CDで変更されたcontractモデルのみテスト
dbt build --selector ci_contract_check --profiles-dir .メリット:
- ✅ 再利用可能: 複雑な選択ロジックを1箇所で管理
- ✅ 可読性:
--selector contract_modelsは意図が明確 - ✅ CI/CD統合: 標準化された選択パターン
参考: YAML Selectors | dbt Developer Hub
8.8 実用例: CI/CDでの活用(Slim CI + Contracts)
# .github/workflows/dbt_pr_check.yml
name: dbt PR Check with Contracts
on:
pull_request:
paths:
- "models/**"
- "dbt_project.yml"
jobs:
dbt-contract-validation:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install dbt
run: pip install dbt-bigquery
- name: Authenticate to BigQuery
uses: google-github-actions/auth@v1
with:
# GitHub環境変数から認証情報を取得
credentials_json: ${{ env.GCP_SA_KEY }}
# ステップ1: Compile check(Contract検証、コスト: 0円)
- name: Compile with contract validation
run: |
dbt compile --select state:modified+ --profiles-dir . --target ci
# Contract違反があればここで失敗
# ステップ2: Slim CI - 変更されたモデルのみテスト
- name: Build changed models with contracts
run: |
dbt build --select state:modified+ --profiles-dir . --target ci --state ./prod_manifest/
# state:modified+ = 変更されたモデルと下流のみ実行
# ステップ3: Contract有効化モデルのunit testsのみ実行
- name: Run unit tests for contract models
run: |
dbt test --select tag:contract_enforced,test_type:unit --profiles-dir . --target ci
- name: Upload logs
if: always()
uses: actions/upload-artifact@v3
with:
name: dbt-logs
path: logs/CI/CDパイプラインの効率化:
| ステップ | 実行内容 | 実行時間 | BigQueryコスト |
|---|---|---|---|
| 1. Compile | Contract検証(全モデル) | 5-10秒 | 0円 |
| 2. Build | 変更モデル + 下流のみ | 1-3分 | 小(必要最小限) |
| 3. Unit Tests | Contract有効モデルのみ | 30秒-1分 | 極小(モックデータ) |
state:modified+ の効果:
- ✅ 変更されたモデルのみ実行(BigQueryコスト削減)
- ✅ 下流モデルも検証(破壊的変更を検出)
- ✅ Contract違反を即座に検出(PR段階でブロック)
参考:
- Get started with Continuous Integration tests | dbt Developer Hub
- Implementing Data Contracts with dbt: From Theory to Practice | Medium
- Data Contracts And Schema Enforcement With Dbt | Xebia
8.9 ベストプラクティス: Contractモデルの選択戦略
| 用途 | 推奨セレクター | 理由 |
|---|---|---|
| ローカル開発 | --select model_name | 開発中のモデルのみ高速実行 |
| pre-commit | dbt compile | Contract検証のみ(コスト: 0円) |
| PR作成時 | --select state:modified+ | 変更モデルと下流のみ(Slim CI) |
| mainマージ前 | --select tag:contract_enforced | Contract有効モデル全体を検証 |
| 本番デプロイ | dbt build | 全モデル + 全tests実行 |
推奨タグ戦略:
# models/schema.yml
models:
- name: fct_orders
config:
contract:
enforced: true
tags:
- contract_enforced # Contract有効化モデル
- prod_critical # 本番重要モデル
- daily # 日次実行効果:
- ✅ 柔軟な選択: 目的に応じてタグを組み合わせ
- ✅ CI/CD効率化: 必要最小限のモデルのみ実行
- ✅ コスト削減: BigQuery実行を最小化
メリット:
- ✅ Contractが有効なモデルだけを選択して実行
- ✅ 失敗が予想されるモデルを除外して効率的にテスト
- ✅ タグを使って運用を柔軟に管理
- ✅ Slim CIでBigQueryコストを最小化
参考: Model selection syntax | dbt Developer Hub
まとめ
Contract + Unit Tests = 堅牢な品質保証
graph TB A[Contract] --> C[出力スキーマ<br/>型安全性] B[Unit Tests] --> D[ロジック<br/>正確性] C --> E[下流破綻防止] D --> E E --> F[データ品質<br/>完全保証]
推奨設定まとめ
| 環境 | Contract | on_schema_change | Unit Tests |
|---|---|---|---|
| 開発 | enforced: true | append_new_columns | 必須 |
| ステージング | enforced: true | append_new_columns | 必須 |
| 本番 | enforced: true | fail | 必須 |
Contractsが検証できる項目(完全版)
| 検証項目 | 検証する | エラー時の動作 |
|---|---|---|
| 列名 | ✅ はい | Compilation Error |
| 列数 | ✅ はい | Compilation Error |
| データ型 | ✅ はい | Compilation Error |
| 列の順序 | ❌ いいえ | dbtが自動調整 |
Contracts vs Constraints
| 項目 | Contracts | Constraints |
|---|---|---|
| 検証タイミング | コンパイル時 | 実行時 |
| BigQueryコスト | 0円 | 発生 |
| 検証内容 | スキーマ構造 | データ値 |
重要な学び
- Contractsはデータ型だけでなく、列名と列数も検証する
- 列の順序は検証されない(dbtが自動調整)
- Contract違反はBigQuery実行前に検出(コスト: 0円)
- Constraintsは別機能(実行時にデータ値を検証)
- 型を明示する: unit testでもCASTを使う
- Contractは必須: incrementalモデルでは特に重要
- スキーマ変更は慎重に:
on_schema_change: failで事故防止 - タグで選択・除外:
--select tag:contract_test --exclude model_name
検証日: 2026-02-17(初回)、2026-02-18(追加検証) 作成者: dbt検証プロジェクト バージョン: 2.0 参考: