CertSafari
    DBT Analytics Engineering· Lessons

    Domain 2 · Lesson 17/31

    dbt constraints in YAML: enforcing data integrity in the warehouse

    Defining constraints in YAML to enforce data integrity at the platform level

    14 min read
    3.23% of exam
    2 sources
    Published 12 Oct 2026
    Docs as of 12 Oct 2026

    What you will be able to do

    • Explain how a constraint declared in YAML ends up being checked by the data platform rather than by dbt
    • State the two prerequisites a model must meet before its constraints are applied
    • Write column-level and model-level constraints, including foreign keys built with ref()
    • Predict which constraint types a given platform defines and enforces, and use warn_unenforced and warn_unsupported correctly
    • Decide when a constraint can replace a data test and when the test must stay

    Key concept

    Enforced constraint — A rule on a column or set of columns that dbt writes into the table's DDL and that the data platform checks while it writes rows. Rows that break the rule never land in the table, because the build fails and is rolled back.

    1.Constraints move validation into the data platform

    A constraint is a rule that the data platform itself checks as rows are written. You declare it in a model's YAML. dbt doesn't evaluate it with a query of its own. Instead, dbt will include the column names, data types, and constraints in the DDL statements it submits to the data platform, so the warehouse checks every row as it creates or updates the table. When a row breaks the rule, the table creation or update fails and the operation is rolled back.

    The difference from a data test is timing. A data test validates a model's content after the model is built, so it inspects a table that already exists. A constraint is checked at build time, and a violation stops the build itself. Here is the Postgres DDL that dbt renders for an id column with not_null, primary_key and check constraints. All three rules end up on the column definition, where Postgres enforces them on the insert that follows.

    Constraints from YAML rendered into the CREATE TABLE statement on Postgressql
    create table "database_name"."schema_name"."constraints_example__dbt_tmp"
    (
        id integer not null primary key check (id > 0),
        customer_name text,
        first_transaction_date date
    )
    ;

    Checkpoint 1 of 8· Check yourself

    You declare a not_null constraint on a column in YAML. What actually checks it?

    Sources12

    2.Prerequisites: a table or incremental model with an enforced contract

    Constraints are only applied when two conditions hold. If either is missing, the YAML is accepted but nothing reaches the warehouse.

    - Materialization. The model must be table or incremental. Constraints are never applied on ephemeral models or on models materialized as view. - Contract. The model must declare and enforce a contract, so every column needs a data_type in YAML, not just the columns you constrain.

    The contract requirement often surprises people. Adding not_null to one key column of a 40-column model means declaring all 40 columns with their types. The contract itself, meaning the name and type checks, its supported materializations and what counts as a breaking change, is covered under [model contracts](lesson:2.1). For constraints, what matters is that the contract is the vehicle that carries them into the DDL.

    Views are a partial exception. A view can carry an enforced contract, but the contract only covers column names and data types. Views offer support for those, but not constraints, so a not_null declared on a view column is not applied.

    The minimum shape: an enforced contract, a data_type on every column, and a not_null constraint on the keyyaml
    models:
      - name: dim_customers
        config:
          contract:
            enforced: true
        columns:
          - name: customer_id
            data_type: int
            constraints:
              - type: not_null
          - name: customer_name
            data_type: string

    Checkpoint 2 of 8· Check yourself

    A model is materialized as view with an enforced contract, and its order_id column declares a not_null constraint. What does the platform enforce?

    Checkpoint 3 of 8· Exam question

    A team runs its warehouse on Postgres and wants dbt to actively reject any build of `fct_orders` that loads a `customer_id` value with no matching row in `dim_customers`, not just document the relationship for humans reading the docs site. Which change to the contracted model's YAML best achieves this?

    Sources12

    3.Writing constraints at the column and model level

    A constraint can be attached to a single column or to the model as a whole. The general rule is to define a single-column constraint directly under that column. Use the model level when the rule spans more than one column, such as a composite primary key, a multi-column foreign key, or a check that compares two columns. One case requires the model level: a model with multiple primary_key constraints has to define them there, because defining multiple primary keys at the column level is not supported.

    Every constraint is built from the same few fields:

    - type (required): one of not_null, unique, primary_key, foreign_key, check or custom. - expression: free text that qualifies the constraint. Some types require it, for example a check needs its boolean condition. - name (optional): a human-friendly name, supported by some data platforms. - columns (model level only): the list of columns the constraint applies to.

    Model-level constraints span several columns, so each one lists its columns explicitlyyaml
        # model-level constraints
        constraints:
          - type: primary_key
            columns: [first_column, second_column, ...]
            warn_unsupported: True # show a warning if unsupported
          - type: foreign_key # multi_column
            columns: [first_column, second_column, ...]
            to: ref('my_model_to') | source('source', 'source_table')
            to_columns: [other_model_first_column, other_model_second_columns, ...]
          - type: check
            columns: [first_column, second_column, ...]
            expression: "first_column != second_column"
            name: human_friendly_name

    Foreign keys take two extra inputs. to names the referenced relation with ref() or source(), and to_columns lists the primary or unique key columns in that relation. Because to uses ref, dbt records the dependency in the DAG and resolves the right schema in every environment. A hard-coded table name would point development builds at production. This syntax is available in dbt v1.9 and later.

    Checkpoint 4 of 8· Fill the gap

    Which key completes this column-level foreign key so that it names the matching column in the referenced model?

            # column-level constraints
            constraints:
              - type: not_null
              - type: unique
              - type: foreign_key
                to: ref('my_model_to') | source('source', 'source_table')
                 ? : [other_model_column]
                warn_unenforced: False # skips warning if supported but not enforced

    Sources1

    4.What each platform defines and enforces

    Writing a constraint in YAML does not mean your warehouse will reject rows that break it. Every constraint type falls into one of three categories on a given platform:

    - Definable and enforced: the model won't build if the data violates it. - Definable and not enforced: the constraint is written into the DDL as metadata, and the model builds even if the data violates it. - Not definable: the platform cannot express the constraint at all.

    Transactional databases enforce strictly, while cloud warehouses mostly record metadata. Postgres enforces everything in the ANSI SQL standard plus row-level check. Most analytical platforms enforce not_null and little else. On Snowflake, unique, primary_key and foreign_key are accepted but not verified on insert. If you specify rely in the constraint expression, Snowflake can use them for query optimization. Unenforced constraints can still be useful for integrating with legacy data catalogs or entity-relationship diagram tools.

    Constraint support on four platforms
    Platformnot_nullprimary_key / foreign_keyuniquecheck
    PostgresDefined and enforcedDefined and enforcedDefined and enforcedDefined and enforced
    RedshiftDefined and enforcedDefined, metadata onlyDefined, metadata onlyNot supported
    SnowflakeDefined and enforcedDefined, metadata onlyDefined, metadata onlyNot supported
    BigQueryDefined and enforcedDefined, metadata onlyNot supportedNot supported

    dbt warns whenever a constraint you wrote won't be enforced on your platform. Two optional fields, set on an individual constraint, turn those warnings off, and they behave differently:

    - warn_unenforced: False silences the warning for a constraint the platform supports but doesn't enforce. The constraint is still included in the DDL. - warn_unsupported: False silences the warning for a constraint the platform doesn't support. That constraint is left out of the DDL.

    On Snowflake, for example, a check constraint triggers a warning and dbt skips it.

    Checkpoint 5 of 8· Match them up

    Match each constraint field to what it does

    Tap a term, then the definition that fits it.

    Checkpoint 6 of 8· Exam question

    A team defines `primary_key`, `foreign_key`, `unique`, and `not_null` constraints on a contracted model that materializes on Snowflake. Which statements accurately describe what happens when this model is built?(Select 3)

    Sources1

    5.Choosing between a constraint and a data test

    A constraint can replace a data test, but only when it is actually enforced. Swapping a not_null test for a not_null constraint guarantees the check at build time and probably needs less compute, because there is no separate query afterward. The swap is only safe when all three of these hold:

    - The platform supports and enforces that constraint. Most platforms only enforce not_null. - The model is materialized as table or incremental. - The model has a full contract, with the name and data_type of every column.

    When those conditions don't hold, the test stays. The usual case is a primary_key on Snowflake, Redshift or BigQuery. The platform rejects null keys but accepts duplicates, so the uniqueness half of the rule still needs a unique test. Tests also have strengths that constraints lack. You can set custom severity thresholds, query the already-built model to debug a failure, and store the failing records in the warehouse.

    Snowflake: keep the primary_key for metadata and add a unique test for the part the platform doesn't checkyaml
        columns:
          - name: id
            data_type: integer
            description: hello
            constraints:
              - type: not_null
              - type: primary_key # not enforced  -- will warn & include
              - type: check       # not supported -- will warn & skip
                expression: "id > 0"
            data_tests:
              - unique            # need this test because primary_key constraint is not enforced

    Checkpoint 7 of 8· Check yourself

    On Snowflake, a contracted table model declares primary_key on order_id. Upstream starts sending duplicate order_id values. How do you make sure the duplicates are caught?

    Checkpoint 8 of 8· Exam question

    A contracted incremental model has run successfully for months. After a new sanctioned column is added to its YAML, the next scheduled build fails on a schema mismatch against the enforced contract. The team wants this and future column additions to build cleanly without loosening the contract. Which action should they take?

    Sources2

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 1.Adding constraints to a view model makes the warehouse reject bad rows.Why is that wrong?

      Constraints only apply to table and incremental models. A view with an enforced contract gets name and type checks but no constraints.

      Covered in Prerequisites: a table or incremental model with an enforced contract

    2. 2.You can add a not_null constraint to one column without declaring a contract or types for the other columns.Why is that wrong?

      Constraints need an enforced contract, which requires a data_type for every column in the model.

      Covered in Prerequisites: a table or incremental model with an enforced contract

    3. 3.A primary_key constraint on Snowflake guarantees that the key column is unique.Why is that wrong?

      Snowflake only checks not-null, including the not-null part of a primary key. Uniqueness is metadata only.

      Covered in What each platform defines and enforces

    4. 4.A composite key can be declared by putting primary_key under each of its columns.Why is that wrong?

      Multiple primary_key constraints must be declared once at the model level, with a columns list.

      Covered in Writing constraints at the column and model level

    Sources

    Every claim above is drawn from one of these pages, quoted as it was written on the date shown.

    1. 1.
      “If the validation fails, the table creation or update fails, the operation is rolled back, and you will see a clear error message.”
      ↩︎ Constraints move validation into the data platform
      “Constraints only work on table and incremental models.”
      ↩︎ Prerequisites: a table or incremental model with an enforced contract
      “This means you need to explicitly define the data_type for every column in your model's schema configuration.”
      ↩︎ Prerequisites: a table or incremental model with an enforced contract
      “As a general rule, we recommend defining single-column constraints directly on those columns.”
      ↩︎ Writing constraints at the column and model level
      “If you define multiple primary_key constraints for a single model, those must be defined at the model level.”
      ↩︎ Writing constraints at the column and model level
      “This syntax for defining foreign keys uses ref, meaning it will capture dependencies and works across different environments.”
      ↩︎ Writing constraints at the column and model level
      “a model can still build even if building the model violates the constraint. This constraint exists for metadata purposes only.”
      ↩︎ What each platform defines and enforces
      “you may optionally instruct Snowflake to use them for query optimization by specifying rely in the constraint expression field.”
      ↩︎ What each platform defines and enforces
      “The constraint will be included in templated DDL.”
      ↩︎ What each platform defines and enforces
      “Redshift currently only enforces not null constraints; all other constraints are metadata only.”
      ↩︎ What each platform defines and enforces
      “When enforced, a constraint guarantees that you will never see invalid data in the table materialized by your model.”
      ↩︎ Key concept
      “Constraints are never applied on ephemeral models or those materialized as view.”
      ↩︎ Exam trap 1
      “To use constraints, your model must declare and enforce a contract.”
      ↩︎ Exam trap 2
      “only the not null (and the not null property of primary key) are actually checked at present.”
      ↩︎ Exam trap 3
      “Defining multiple primary_key constraints at the column level is not supported.”
      ↩︎ Exam trap 4
      “warn_unsupported: False to skip warning on constraints that aren't supported by this data platform, and therefore won't be included in templated DDL.”
      ↩︎ Checkpoint
      “need this test because primary_key constraint is not enforced”
      ↩︎ Checkpoint
    2. 2.
      “dbt will include the column names, data types, and constraints in the DDL statements it submits to the data platform”
      ↩︎ Constraints move validation into the data platform
      “Data Tests are a more flexible mechanism for validating the content of your model after it's built.”
      ↩︎ Constraints move validation into the data platform
      “views offer support for column names and data types, but not constraints”
      ↩︎ Prerequisites: a table or incremental model with an enforced contract
      “This has the advantage of guaranteeing the validation at build time, and it probably requires less compute (cost) in your data platform.”
      ↩︎ Choosing between a constraint and a data test
      “Making sure that your data platform can support and enforce the constraint that you need. Most platforms only enforce not_null.”
      ↩︎ Choosing between a constraint and a data test
      “They are easier to debug after finding failures because you can query the already-built model, or store the failing records in the data warehouse.”
      ↩︎ Choosing between a constraint and a data test

    Spotted a mistake, or was something unclear? Tell us.