Some dbt adapters let you attach data warehouse constraints (NOT NULL, PRIMARY KEY, UNIQUE, CHECK, etc.) when models are built. This blocks invalid data from being written at runtime.
However, if you misjudge how constraints, dbt tests (generic tests), and model contracts divide responsibilities, you can unintentionally crash production jobs — or weaken your quality guarantees in the other direction. This article covers how to declare constraints, the differences between adapters, migration playbooks, and exam-relevant takeaways.
The dbt constraints feature creates database constraints during CREATE/ALTER TABLE when the adapter (Snowflake, Databricks, etc.) supports it. The result: at load time (INSERT/MERGE), invalid data cannot be written.
dbt tests aggregate or scan data after loading to verify quality. Constraints are "prevention", tests are "detection". Model contracts (contracts.enforced) enforce schema consistency (column names and types) — they are separate from value-level constraints.
| Aspect | dbt tests | Constraints | Model contracts |
|---|---|---|---|
| Where defined | schema.yml (generic tests) | Model config or schema.yml (when adapter supports it) | Model config (contracts.enforced: true) |
| When it runs | After the build | At DML time, by the warehouse | At build time, validating DDL vs. query |
| Failure behavior | Test fails (job behavior depends on config) | The write errors out and aborts | The build errors out (column name/type mismatch) |
| Primary purpose | Continuous quality detection | Block invalid data at runtime | Enforce the schema contract |
How constraints, tests, and contracts flow inside dbt
When the adapter supports it, you can declare constraints in the model's config or schema.yml. The declarations are applied during DDL generation. Behavior for existing tables depends on the adapter: in some cases ALTER TABLE attaches the constraint, in others a rebuild (--full-refresh) is required.
The actual enforcement of PRIMARY KEY and UNIQUE differs by warehouse. For example, Snowflake enforces NOT NULL, but PRIMARY KEY/UNIQUE are often informational (not enforced). Databricks (Delta Lake) can enforce NOT NULL and CHECK, but native PRIMARY KEY support is limited.
Declaration example (model config and 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 (also keep tests as a safety net)
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 is enforced by the engine. PRIMARY KEY and UNIQUE are informational, so they do not necessarily guarantee query optimization or runtime duplicate rejection. In practice, combine NOT NULL constraints with dbt's unique test, or with deduplication logic in the model itself.
Databricks (Delta Lake): NOT NULL and CHECK constraints can be enforced. PRIMARY KEY support is limited, so uniqueness is typically guaranteed by model-side deduplication or by ensuring the merge key is unique.
CHECK as a stand-in for uniqueness in Delta Lake
-- Databricks Delta example: validate email format or range
{{ config(
materialized='table',
constraints=[
{ 'type': 'not_null', 'columns': ['customer_id', 'email'] },
{ 'type': 'check', 'expression': "email LIKE '%@%._%'" }
]
) }}
select * from {{ ref('stg_customers') }};Incremental models often perform MERGE using unique_key, and a NOT NULL constraint on that column acts as a safety net. Because adding or changing a constraint may force a table rebuild on some adapters, plan --full-refresh runs deliberately.
Constraints are usually attached at table creation. Whether an ALTER can add one to a live table — and whether dbt emits that ALTER automatically — depends on the adapter implementation. For change management, validate in staging first, and roll out to production starting with the lowest-risk constraints (NOT NULL).
Example config for an incremental model
{{ 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() %}
-- incremental filter (e.g. by updated_at)
where updated_at > (select coalesce(max(updated_at), '1970-01-01') from {{ this }})
{% endif %}If you want to add NOT NULL to an existing table, first finish backfilling or correcting NULLs and confirm dbt's not_null test is green, then attach the constraint. For production, the safe sequence is: apply to staging, then run --full-refresh in production during a low-traffic window.
Expressing intent with PRIMARY KEY or UNIQUE is useful, but in environments where enforcement is limited (e.g. Snowflake), guarantee uniqueness through model-side deduplication (window functions, QUALIFY, etc.) combined with tests.
Deduplication example (keep only unique rows)
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 = 1Being able to verbalize the division of labor among constraints, tests, and contracts is critical. In particular, the adapter differences — Snowflake's PK/UNIQUE being informational, Snowflake enforcing NOT NULL, Delta enforcing NOT NULL/CHECK strongly — show up often.
Beware the trap that "to guarantee uniqueness, just declare PK". In real implementations, model-side deduplication plus tests is usually the most reproducible approach.
Analytics Engineer
問題 1
You are targeting Snowflake with dbt. You want to disallow NULLs on customer_id in the customers model, block invalid rows at write time, and also preserve quality observability. Which is the most appropriate approach?
正解: A
On Snowflake, NOT NULL is enforced by the engine and blocks NULL writes at runtime. Keeping dbt's not_null test for observability is the most practical setup. PK alone does not guarantee that NULLs or duplicates are blocked at runtime, and contracts enforce column name/type consistency — NULL value checks are handled separately.
Should I prioritize dbt constraints or dbt tests?
They serve different roles. Constraints block writes at runtime (prevention), while tests detect issues after the build runs (observability). Use both whenever possible — for NOT NULL in particular, we recommend layering a constraint and a test together.
In Snowflake, will declaring PRIMARY KEY or UNIQUE reject duplicates?
In Snowflake, PRIMARY KEY and UNIQUE are largely informational and do not necessarily reject duplicates at runtime. The common approach is to enforce uniqueness through model-side deduplication logic combined with dbt's unique test.
If I want to add a constraint later, do I have to drop the table?
It depends on the adapter implementation. Some adapters can add constraints via ALTER, but other changes require --full-refresh. Validate in staging first and plan a maintenance window for production.
Practice with certification-focused question sets
無料で問題を解いてみるNicheeLab Editorial Team
NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.
dbt Models: SQL-Defined Transformation Units (2026)
Model fundamentals — SELECT-based definitions, naming, refs,...
dbt Analytics Engineering Exam: Complete Guide (2026)
Pass the AE Certification — scope, weighting, sample questio...
dbt Cloud vs dbt Core: Feature & Cost Comparison (2026)
Honest comparison of dbt Cloud vs. dbt Core — IDE, scheduler...
dbt Project Structure: models/seeds/macros Layout (2026)
Recommended dbt project layout — models, seeds, macros, snap...
dbt_project.yml Explained: Every Config (2026)
Every dbt_project.yml setting that matters — paths, vars, ma...