dbtはモデル作成時にデータウェアハウスの制約(NOT NULL、PRIMARY KEY、UNIQUE、CHECKなど)を付与できるアダプタがあります。これにより、実行時に不正データの書き込みをブロックできます。
一方で、dbtのテスト(generic tests)やモデルコントラクト(contracts)との役割分担を誤ると、意図せず本番ジョブが落ちたり、逆に品質保証が弱くなったりします。本稿は、制約の宣言方法、アダプタ差、移行運用、試験対策の観点で実務に落ちる線を解説します。
dbtのconstraints機能は、アダプタ(SnowflakeやDatabricksなど)が対応している場合に、CREATE/ALTER TABLE時にデータベース制約を作成します。これにより、ロード時(INSERT/MERGE)に不正データを書き込めなくなります。
dbtのテストは、ロード後に集計・走査して品質を検証します。制約は“予防”、テストは“検知”の役割です。モデルコントラクト(contracts.enforced)はスキーマの整合(列名・型)を強制する仕組みで、値制約とは別物です。
| 観点 | dbtテスト | 制約(constraints) | モデルコントラクト(contracts) |
|---|---|---|---|
| 定義の場所 | schema.yml(generic tests) | モデルconfigやschema.yml(アダプタ対応時) | モデルconfig(contracts.enforced: true) |
| 実行タイミング | ビルド後に検査 | DML時にウェアハウスが検査 | ビルド時にDDLとクエリ整合を検査 |
| 失敗時の挙動 | テストがfail(ジョブは設定次第) | 書き込みがエラーで中断 | ビルドがエラー(列名・型不一致) |
| 主目的 | 品質の継続的検知 | 実行時の不正データの遮断 | スキーマ契約の強制 |
dbtにおける制約・テスト・コントラクトの流れ
アダプタが対応している場合、モデルのconfigやschema.ymlで制約を宣言できます。宣言はDDL生成時に反映されます。既存テーブルに対する追加はアダプタの挙動に依存し、ALTER TABLEで付与される場合と、再作成(--full-refresh)が必要な場合があります。
PRIMARY KEYやUNIQUEの実効性はウェアハウスごとに異なります。たとえばSnowflakeではNOT NULLは強制されますが、PRIMARY KEY/UNIQUEは情報的(enforcedされない)であることがあります。Databricks(Delta Lake)はNOT NULLやCHECKを強制できますが、PRIMARY KEYはネイティブサポートが限定的です。
宣言例(モデルconfigとschema.yml)
-- models/customers.sql
{{ config(
materialized='table',
constraints=[
{ 'type': 'primary_key', 'columns': ['customer_id'] },
{ 'type': 'not_null', 'columns': ['customer_id', 'email'] },
{ 'type': 'unique', 'columns': ['email'] }
],
contracts={'enforced': true}
) }}
select
customer_id::number as customer_id,
email::string as email,
created_at::timestamp as created_at
from {{ ref('stg_customers') }};
# schema.yml(補助的にテストも併用)
models:
- name: customers
columns:
- name: customer_id
tests:
- not_null
- unique
constraints:
- type: not_null
- type: primary_key
- name: email
tests:
- not_null
constraints:
- type: not_nullSnowflake: NOT NULLはエンジンで強制されます。PRIMARY KEYやUNIQUEは情報的で、クエリ最適化や実行時の重複拒否を必ずしも保証しません。そのため、実運用ではNOT NULL制約とdbtのuniqueテスト、もしくはモデル側の重複排除ロジックを併用します。
Databricks(Delta Lake): NOT NULLとCHECK制約を強制できます。PRIMARY KEYは限定的で、ユニーク性の保証はモデルでの去重やマージキーの一意性担保で実現するのが一般的です。
Delta LakeでのCHECK代替例(疑似ユニーク制約)
-- Databricks Deltaの例:メールの形式や範囲チェック
{{ config(
materialized='table',
constraints=[
{ 'type': 'not_null', 'columns': ['customer_id', 'email'] },
{ 'type': 'check', 'expression': "email LIKE '%@%._%'" }
]
) }}
select * from {{ ref('stg_customers') }};インクリメンタルモデルではunique_keyを用いたMERGEを行うケースが多く、NOT NULL制約はそのカラムに対する安全装置になります。制約の追加・変更は、アダプタによってはテーブル再作成が必要となるため、--full-refreshの計画的な実行が求められます。
制約はテーブル作成時に付与されるのが基本です。運用中のテーブルにALTERで追加できるか、dbtが自動でALTERを発行するかはアダプタ実装依存です。変更管理では、ステージング環境で先に制約を試し、本番は低リスクのNOT NULLから段階導入するのが無難です。
インクリメンタルモデルでの設定例
{{ config(
materialized='incremental',
unique_key='customer_id',
on_schema_change='sync_all_columns',
constraints=[ { 'type': 'not_null', 'columns': ['customer_id'] } ]
) }}
with src as (
select * from {{ ref('stg_customers') }}
)
select * from src
{% if is_incremental() %}
-- 追加ロジック(例:更新日時で絞り込み)
where updated_at > (select coalesce(max(updated_at), '1970-01-01') from {{ this }})
{% endif %}既存テーブルにNOT NULLを追加する場合は、事前にNULL埋めや補正を完了し、dbtのnot_nullテストでグリーンを確認した上で制約を付与します。本番ではまずステージングで適用→本番は低トラフィック時間帯に--full-refresh、が安全です。
PRIMARY KEYやUNIQUEの“意図表明”は有用ですが、Snowflakeのように実効性が限定的な環境では、モデル側での去重(window関数・QUALIFYなど)とテストの併用で一意性を保証します。
去重の例(一意行のみ残す)
with base as (
select *,
row_number() over (partition by customer_id order by updated_at desc) as rn
from {{ ref('stg_customers') }}
)
select * from base where rn = 1制約・テスト・コントラクトの役割分担を言語化できることが重要です。特にSnowflakeのPK/UNIQUEが情報的である点、NOT NULLは強制される点、DeltaはNOT NULL/CHECKが強い点など、アダプタ差分は頻出です。
“ユニーク性を保証したい=PK制約で完璧”とは限らない、という落とし穴に注意。現実装ではモデルの去重ロジック+テストの併用が最も再現性が高いケースが多いです。
Analytics Engineer
問題 1
dbtでSnowflakeをターゲットにしており、customersモデルのcustomer_idにNULLを許さず、実行時に不正行の書き込みをブロックしたい。さらに品質可観測性も維持したい。最も適切な対応はどれか。
正解: A
SnowflakeではNOT NULLはエンジンで強制され、実行時にNULLの書き込みをブロックできます。可観測性のためにdbtのnot_nullテストも維持するのが実務的に最適です。PKのみでNULL/重複が実行時にブロックされるとは限らず、contractsは列名・型の整合を強制する仕組みで値のNULLは別管理です.
dbtの制約とテストはどちらを優先すべきですか?
役割が異なります。制約は実行時のブロック(予防)、テストはビルド後の検知(可観測性)です。可能なら両方を使い、特にNOT NULLは制約+テストの二段構えを推奨します。
SnowflakeでPRIMARY KEYやUNIQUEを宣言すれば重複は拒否されますか?
SnowflakeではPRIMARY KEY/UNIQUEは情報的であるケースが多く、実行時に必ずしも重複を拒否しません。ユニーク性はモデル側の去重処理とdbtのuniqueテストで担保する運用が一般的です。
制約を後から追加したい場合、テーブルを落とす必要はありますか?
アダプタ実装によります。ALTERで付与できる場合もありますが、変更内容によっては--full-refreshが必要です。ステージングでの事前検証と本番のメンテナンス計画を用意してください。
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)、設定優先度...