When a model's output schema drifts, downstream dashboards and ML pipelines break. dbt Model Contracts make the column structure and data types of a model explicit as a "contract" so mismatches between implementation and schema are detected and prevented early.
This article, based on stable features in the official dbt documentation, organizes how to write contracts, their runtime behavior, how they differ from tests and DB constraints, and the key points for CI implementation, from both the Analytics Engineer exam perspective and real-world operations. Refer to dbt Docs for detailed specifications and the latest adapter support.
Model Contracts let you declare the output schema of a model (column names, order, and data types) in YAML. When contract.enforced is turned on, dbt adjusts the SQL at build time (projecting and casting) to satisfy the contract, and fails the build on any mismatch. This prevents upstream changes from propagating silently to downstream consumers.
There are three main benefits: 1) schema lock-in (early detection of breakage), 2) explicit types (suppressing implicit casts), and 3) easier change review (diffs surface in YAML). Note that ensuring the quality of the data values themselves is the realm of tests and DB constraints.
Contract flow (development → CI → execution platform)
Contracts are declared in a model's YAML file with column definitions and types, and applied by enabling contract.enforced. Write data types using the type names of the warehouse you are using. Type aliases are accepted by many adapters, but avoid ambiguous expressions and verify with compile/build in the actual environment for safety.
Contracts can be enabled per model or per package (via the model selector in dbt_project.yml). A practical organizational standard is to require contracts on every public model that is consumed downstream (for example, marts).
Example: enabling contracts in schema.yml and dbt_project.yml
version: 2
models:
- name: dim_customers
description: Customer dimension (public)
config:
contract:
enforced: true
columns:
- name: customer_id
description: Customer ID unique within the business
data_type: BIGINT # Snowflake: NUMBER(38,0) also works
tests:
- not_null
- unique
- name: customer_name
data_type: VARCHAR
- name: is_active
data_type: BOOLEAN
# dbt_project.yml (enable contracts by default under marts)
models:
my_project:
marts:
+contract:
enforced: trueWhen the contract is enforced, dbt projects columns in the declared order from the model's SELECT and inserts type casts as needed, within what the adapter allows. Extra columns in the SELECT that are not part of the contract are dropped from the output. Missing columns or type mismatches that cannot be cast cause the build to fail.
Views also have their schema locked by column projection and casts. When creating a table, CREATE TABLE is performed using the declared types, typically followed by INSERT or CREATE OR REPLACE (the exact DDL depends on the adapter).
| Feature | Primary purpose | When it runs | Behavior on failure |
|---|---|---|---|
| Model Contracts | Lock down schema (names, order, types) | When dbt build/run materializes the model | Mismatches fail the job. Extra columns are excluded; missing columns or unsupported casts error out |
| dbt Tests | Validate value quality and relationships | When dbt test runs (often in CI) | Reports test failures. Model materialization has often already succeeded |
| DB constraints (NOT NULL, PK, etc.) | Ensure consistency at the database level | When DML/DDL is applied (evaluated by the DB) | The DB rejects the insert/update and the transaction fails |
Example: specifying config directly inside the SQL model (casts are inserted depending on the adapter)
{{ config(materialized='view', contract={'enforced': True}) }}
with src as (
select * from {{ ref('stg_customers') }}
)
select
cast(customer_id as bigint) as customer_id,
cast(customer_name as varchar) as customer_name,
cast(active as boolean) as is_active
from srcTables and views are generally the primary candidates for applying contracts. Views also have their schema locked by projection and casts, but if a type change in the underlying table propagates and becomes uncastable, the build fails.
For incremental models, adding columns or changing types on an existing table depends on the database's DDL compatibility. When you add a column via the contract, recreating with full-refresh as needed is the safer operational choice.
Operational tip: release procedure notes by change type
- Compatible addition (appending columns): add to YAML first → build → notify downstream
- Compatible removal (removing unused columns): remove downstream references → remove from YAML → build
- Incompatible (type or column rename): fix downstream on the same branch → validate together in CI → full-refresh in productionContracts lock down "the shape of the output." Value quality is the job of tests, and enforcement on actual data operations is the job of DB constraints. Layering all three makes design intent explicit in YAML/DDL/tests and makes changes visible.
In practice, a stable approach is: require contracts on public models, ensure primary/foreign-key equivalents via tests, and leverage DB constraints (NOT NULL, unique) where possible.
Example: combining contracts and tests (ensure referential integrity with relationships)
version: 2
models:
- name: fct_orders
config:
contract:
enforced: true
columns:
- name: order_id
data_type: BIGINT
tests: [unique, not_null]
- name: customer_id
data_type: BIGINT
tests:
- not_null
- relationships:
to: ref('dim_customers')
field: customer_id
- name: order_total
data_type: NUMERICMake it mandatory to run dbt build on every PR and block contract mismatches. Contract changes (column additions, type changes, removals) always surface as YAML diffs in the PR, which makes review easier. Combined with catalog and documentation generation, you can deliver an API-like public contract.
To strengthen breakage detection, combine selectively running the public layer (marts) as the target, detecting schema changes in seeds and sources, and periodic full-refresh runs (to pay down incremental debt).
Example: a rough CI step in GitHub Actions
steps:
- run: dbt deps
- run: dbt build -s state:modified+ --profiles-dir .
- run: dbt test -s tag:public
# Block the PR on contract mismatches or test failuresAnalytics Engineer
問題 1
You want to add a new column customer_tier (string type) to the public model dim_customers. Which is the most appropriate procedure to release it safely using dbt Model Contracts while preserving downstream compatibility?
正解: A
The contract treats the columns declared in YAML as the source of truth. The safe path is to add the column to schema.yml with the appropriate data_type first, verify in CI that the contract is consistent and the type casts succeed, and then release. Changing only the SQL or deferring the contract leads to missed downstream compatibility issues.
Can data_type use dbt's generic type names?
No. You should generally use the native type name of the warehouse you are using. Many adapters accept aliases, but to avoid ambiguity, run dbt build in your actual environment and verify the generated DDL and casts (see dbt Docs).
Why does the build pass when extra columns appear in SELECT even though a contract is enforced?
When the contract is enforced, only the declared columns are projected in the declared order, so columns outside the contract are dropped from the output. This is by design to preserve downstream compatibility. On the other hand, missing columns or impossible type casts cause the build to fail.
How should type changes be handled in incremental models?
Type changes on an existing table depend on the database's ALTER compatibility and carry significant operational risk. Plan to update the contract and then recreate the table with full-refresh as a rule, providing a migration window when necessary.
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...