学べること

  • 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の基本
  2. Unit Testsとの組み合わせ
  3. データ型の厳密な管理
  4. 実践例と検証
  5. ベストプラクティス
  6. トラブルシューティング

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

効果:

  1. Contract: YAMLで定義した型(date, int64, numeric)を強制
  2. Unit Test: ロジックの正確性(集計処理)を検証
  3. 型安全性: 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: timestamp

Contractなしの場合:

✅ モデル実行成功
   → 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: FLOAT64

3.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_amountFLOAT64 で計算されているが、Contractでは NUMERIC(12,2) を期待

修正:

-- ❌ Bad
select
    quantity * unit_price as total_amount  -- FLOAT64になる
 
-- ✅ Good
select
    cast(quantity * unit_price as numeric) as total_amount

4.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 5

Contract定義: _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

重要な発見:

  1. Compilation Errorとして検出 = BigQueryにクエリを送信する前にdbtがエラーを検出
  2. BigQueryコストがかからない = 実行前にローカルでチェック
  3. 詳細なエラーメッセージ = どの列の型が不一致かを表形式で表示
  4. pre-commit/CIで効率的にチェック可能 = 実行コストなしで型安全性を保証

検証2: Contract正常動作の確認

モデル定義: contract_valid_model.sql

-- 型が正しく一致するケース
select
  customer_id,  -- INT64(契約と一致)
  first_name,   -- STRING(契約と一致)
  last_name     -- STRING(契約と一致)
from {{ ref('stg_customers') }}
limit 5

Contract定義:

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 Error1.67s0円(実行前検出)
Contract正常動作✅ CREATE TABLE4.01s~0.001円
unit test + Contract✅ PASS4.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[✅ 成功]

重要な学び:

  1. Contract違反は Compilation Error = BigQuery実行前に検出される
  2. BigQueryコストがかからない = ローカルで型チェックが完結
  3. pre-commit/CIでの効率的なチェック = 実行コストなしで型安全性を保証可能
  4. 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-commitdbt compileSQL構文+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/

実行フロー:

  1. dbt compile → Contract違反で即座に失敗(コスト0円)
  2. unit tests → ロジック検証(コスト小)
  3. dbt run(変更モデル) → 実際のBigQuery実行(コスト中)

運用戦略の推奨構成

パターンA: コスト重視(推奨)
チェックポイント実行内容理由
pre-commitdbt compile のみコスト0円、高速
CI: PR作成dbt compile + unit tests軽量、必要最小限
CI: mainマージ前全tests + 変更モデル実行完全検証

メリット:

  • ✅ BigQueryコストを最小化
  • ✅ 高速フィードバック(pre-commit: 3秒)
  • ✅ Contract違反を早期発見

デメリット:

  • ⚠️ 実データでの検証は後回し

パターンB: 品質重視
チェックポイント実行内容理由
pre-commitdbt 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マージ前: 完全検証(品質保証)

推奨理由:

  1. 開発速度を維持: pre-commitは高速(3秒)
  2. コストを抑制: 早期段階はBigQueryアクセス最小限
  3. 品質を保証: mainマージ前は完全検証
  4. 段階的チェック: 問題を早く、安く発見

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_active

6. トラブルシューティング

問題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を明示的にCAST

7. Contractsが検証できる項目の完全リスト(2026-02-18 追加検証)

7.1 検証項目の実測結果

検証日時: 2026-02-18 15:20-15:30 JST

#検証項目検証する/しない実行結果エラーメッセージ
1列名✅ 検証するFAILmissing in definition / missing in contract
2列数✅ 検証するFAILmissing in definition
3データ型✅ 検証するFAILdata type mismatch
4列の順序検証しないPASSdbtが自動調整

重要な発見:

  • 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=1

Contracts vs Constraints:

項目ContractsConstraints
検証タイミングコンパイル時実行時(データ挿入時)
検証内容スキーマ構造(列名・型・数)データ値(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_constraints

8.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=2

8.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=3

8.4 使えるセレクター一覧

セレクター説明
--select tag:Xタグで選択--select tag:contract_test
--select model_nameモデル名で選択--select contract_valid
--select config.X:YConfig値で選択--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 .

グラフ演算子の優先順位:

  1. Selector methods (tag:, config:, model名)
  2. Graph operators (+, @)
  3. 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. CompileContract検証(全モデル)5-10秒0円
2. Build変更モデル + 下流のみ1-3分小(必要最小限)
3. Unit TestsContract有効モデルのみ30秒-1分極小(モックデータ)

state:modified+ の効果:

  • 変更されたモデルのみ実行(BigQueryコスト削減)
  • 下流モデルも検証(破壊的変更を検出)
  • Contract違反を即座に検出(PR段階でブロック)

参考:


8.9 ベストプラクティス: Contractモデルの選択戦略

用途推奨セレクター理由
ローカル開発--select model_name開発中のモデルのみ高速実行
pre-commitdbt compileContract検証のみ(コスト: 0円)
PR作成時--select state:modified+変更モデルと下流のみ(Slim CI)
mainマージ前--select tag:contract_enforcedContract有効モデル全体を検証
本番デプロイ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/>完全保証]

推奨設定まとめ

環境Contracton_schema_changeUnit Tests
開発enforced: trueappend_new_columns必須
ステージングenforced: trueappend_new_columns必須
本番enforced: truefail必須

Contractsが検証できる項目(完全版)

検証項目検証するエラー時の動作
列名✅ はいCompilation Error
列数✅ はいCompilation Error
データ型✅ はいCompilation Error
列の順序❌ いいえdbtが自動調整

Contracts vs Constraints

項目ContractsConstraints
検証タイミングコンパイル時実行時
BigQueryコスト0円発生
検証内容スキーマ構造データ値

重要な学び

  1. Contractsはデータ型だけでなく、列名と列数も検証する
  2. 列の順序は検証されない(dbtが自動調整)
  3. Contract違反はBigQuery実行前に検出(コスト: 0円)
  4. Constraintsは別機能(実行時にデータ値を検証)
  5. 型を明示する: unit testでもCASTを使う
  6. Contractは必須: incrementalモデルでは特に重要
  7. スキーマ変更は慎重に: on_schema_change: failで事故防止
  8. タグで選択・除外: --select tag:contract_test --exclude model_name

検証日: 2026-02-17(初回)、2026-02-18(追加検証) 作成者: dbt検証プロジェクト バージョン: 2.0 参考: