dbt

Mastering dbt Model Versions: Operations for Shipping Breaking Changes Safely

2026-04-19
NicheeLab Editorial Team

dbt model versions are the official mechanism for rolling out breaking changes safely. By referencing models with an explicit v and migrating consumers through a compatibility layer, you get incident-free releases and instant rollback in the same workflow.

This article hits the exam-relevant points, while also covering the cutover order and compatibility-view patterns that actually trip people up in the field. Everything is based on the stable behavior documented in the official dbt docs.

Why Version Models, and the Big Picture

Analytics models tend to live for a long time, so breaking changes like column drops and semantic shifts are unavoidable. dbt model versions give you a way to run the old and new versions side by side and migrate consumers on a planned schedule.

The strategy is simple. Producers (upstream) build both versions at once. Consumers pin to a specific version using ref. External-facing stable names are served by compatibility views, and cutover is just swapping the view's underlying reference.

  • Roll out breaking changes incrementally with instant rollback
  • Control the migration timing per consumer via pinned references
  • Combine with schema contracts to enforce safety
Operations PatternProsRisks / Caveats
Direct replace under a fixed name (Create or Replace)Simple and fast operationallyA breaking change instantly breaks every consumer; rolling back is hard
Coexisting versions (versioned models)Plan migration per consumerLong coexistence costs money — plan the cleanup
Stable name served by compatibility viewCutover is just swapping the view target — instant and safeMore view management; verify privileges and dependencies

End-to-end view of coexistence and staged cutover

sources/stagingmodel v1 (existing consumers)compat view (stable name)model v2 (new version, parallel)Consumer A (pinned to v1)Consumer B (pinned to v2)

Example of swapping a compatibility view (the same idea works on Snowflake and Databricks)

-- Switch the stable-name compatibility view from v1 to v2.
-- Existing queries always read the stable name; only the view changes.
CREATE OR REPLACE VIEW analytics.dim_customers AS
SELECT * FROM analytics.versioned.dim_customers_v2;

Defining and Referencing: Declare in YAML, Specify the Version in ref

Model versions are declared in YAML properties. You can give each version its own description, tests, and contract. On the reference side, you pin the dependency by passing the version to ref explicitly.

Organizationally, the safe pattern is two-layered: upstream and intermediate models always pin an explicit version with ref, while external-facing names are delegated to compatibility views.

  • Declare v as an integer under versions; manage description and tests per version
  • Pass the version to ref to pin the dependency
  • For external publishing and BI connections, route through a stable-name compatibility view

YAML and ref example

# models/sources_and_marts.yml
models:
  - name: dim_customers
    description: Customer dimension (stable name exposed via compat view)
    versions:
      - v: 1
        description: Legacy schema (email nullable)
        tests:
          - dbt_utils.unique_combination_of_columns:
              combination_of_columns: [customer_id]
      - v: 2
        description: New schema (email NOT NULL, country_code added)
        tests:
          - dbt_utils.unique_combination_of_columns:
              combination_of_columns: [customer_id]
          - not_null:
              column_name: email

-- models/dim_customers_v2.sql (example)
-- Definition for v=2. The SQL file can live anywhere, but naming
-- it with the version suffix keeps operations clean.
select ...

-- Downstream reference (pinned explicitly to v=2)
select * from {{ ref('dim_customers', v=2) }}

Use Schema Contracts to Codify the "Don't Break This" Promise

The key to version operations is writing down the contract you must not break. Setting dbt contracts to enforced fails the build if column order, type, or presence drifts from the model definition.

When you introduce a breaking change, declare a new version that satisfies its own contract, run it side by side, and retire the old version once all consumers have migrated.

  • Set contracts to enforced: true to lock in types, NOT NULL, etc.
  • Always make breaking changes a new version — never replace the old one outright
  • Pair with tests (unique, not_null) and verify in CI

Contract declaration example (enforced per version)

# models/marts.yml
models:
  - name: dim_customers
    versions:
      - v: 1
        config:
          contract:
            enforced: true
        columns:
          - name: customer_id
            data_type: integer
            tests: [not_null]
          - name: email
            data_type: varchar
      - v: 2
        config:
          contract:
            enforced: true
        columns:
          - name: customer_id
            data_type: integer
            tests: [not_null]
          - name: email
            data_type: varchar
            tests: [not_null]
          - name: country_code
            data_type: varchar

Building the Compatibility Layer: Keep the Stable Name, Swap the Target

Expose external stable names (e.g. analytics.dim_customers) through a compatibility layer. On Snowflake and Databricks, the standard approach is a view, and cutover is a CREATE OR REPLACE VIEW that swaps the reference from v1 to v2.

Bake the cutover into your runbook and put a checklist around it: privileges, metadata, and downstream dependencies (BI tool caches and so on).

  • Stable names are always views; physical tables coexist with versioned suffixes
  • Use a transaction or a single DDL to minimize cutover downtime
  • Always have a rollback procedure ready (point the view back to the old version)

Compatibility-view examples by platform

-- Snowflake
CREATE OR REPLACE VIEW analytics.dim_customers AS
SELECT * FROM analytics.versioned.dim_customers_v1;  -- before cutover

-- Cutover
CREATE OR REPLACE VIEW analytics.dim_customers AS
SELECT * FROM analytics.versioned.dim_customers_v2;  -- after cutover

-- Databricks (Unity Catalog)
CREATE OR REPLACE VIEW analytics.dim_customers AS
SELECT * FROM analytics_versioned.dim_customers_v2;

Safe Release and Rollback Procedure

Releases go in this order: prepare → run in parallel → cut over → monitor → retire. Build and validate v2, then cut over the compatibility view for a limited set of consumers first; if everything looks good, expand to the rest. If anything breaks, point the compatibility view back to v1 immediately.

If incremental models or snapshots are involved, plan for a full refresh or a backfill the first time v2 builds.

  • Prepare: build v2 and pass contracts and tests in CI
  • Parallel: build v1 and v2 together; validate with a limited set of users
  • Cut over: point the compat view to v2; watch errors and metric diffs
  • Rollback: point the view back to v1 — instant recovery
  • Retire: once nothing depends on v1, delete it on a planned schedule

Typical commands and procedure notes

# 1) Build and test just the v2 model up front
#    (Adjust the selector to your environment, e.g. by model name or tag)
dbt run  --select dim_customers+  # build with dependencies
dbt test --select dim_customers

# 2) Swap the compatibility view target (rollback is instant)
-- CREATE OR REPLACE VIEW analytics.dim_customers AS SELECT * FROM ..._v2;

# 3) If something is wrong, point it back
-- CREATE OR REPLACE VIEW analytics.dim_customers AS SELECT * FROM ..._v1;

Exam-Favorite Angles and Real-World Pitfalls

The exam loves to test the safe procedure for breaking changes, version specification in ref, and the role of contracts. In the field, people reflexively replace the stable name directly, so designing operations around compatibility views is what separates good teams from accidents.

To avoid migrations that drag on forever, bake the retirement plan into the design from day one: deadline, affected-party notification, and metric monitoring.

  • Pin the dependency by specifying the version explicitly in ref
  • Use contracts enforced to catch schema breakage in CI
  • Expose stable names through compat views to make cutover and rollback fast
  • Prepare a retirement plan and telemetry (users, query counts)

Anti-pattern vs. good-pattern examples

# Anti-pattern: change everything in place
#   CREATE OR REPLACE TABLE analytics.dim_customers AS ...  -- breaking change
# Good pattern: coexistence + compat view
#   1) Build dim_customers_v2
#   2) Swap the analytics.dim_customers compat view to v2
#   3) Monitor; if anything breaks, point the view back to v1

Check Your Understanding

Analytics Engineer

問題 1

You're introducing a breaking change (column drop + type change) into a dbt model. Which procedure is safest and easiest to roll back?

  1. Run the old and new models side by side, pin downstream consumers to the old version with ref, and swap the compatibility view (stable name) from v1 to v2 only at cutover time
  2. Directly CREATE OR REPLACE the table at the stable name. If something breaks, re-run to revert
  3. Delete the old version immediately, publish only the new one, and restore from backup if it fails
  4. Publish the new version and drop the version argument from every downstream ref so they auto-follow the latest

正解: A

Coexistence, pinned refs, and a swappable compatibility view give you both staged rollout and instant rollback. Direct replacement or immediate deletion has an unbounded blast radius and is hard to recover from. Auto-following the latest version is just an incident waiting to happen.

Frequently Asked Questions

What happens if I don't specify a version in ref?

As an organizational policy, we recommend always specifying the version explicitly to avoid unintended schema changes. This pins the dependency and lets you control exactly when each consumer migrates.

Should I include the version in physical relation names (table/view names)?

Yes. Run the physical relations side by side with versioned names (e.g. dim_customers_v1, dim_customers_v2) and expose a stable name through a compatibility view. That makes both cutover and rollback simple.

Are there platform-specific gotchas for Snowflake or Databricks?

The core principle is the same on every warehouse: expose stable names through a view, and switch with a single DDL like CREATE OR REPLACE VIEW. Make sure your runbook covers privilege handoff, refresh of dependent materialized views and caches, and fully-qualified schema names (catalog/schema).

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

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.


Related articles
dbt

dbt Models: SQL-Defined Transformation Units (2026)

Model fundamentals — SELECT-based definitions, naming, refs,...

dbt

dbt Analytics Engineering Exam: Complete Guide (2026)

Pass the AE Certification — scope, weighting, sample questio...

dbt

dbt Cloud vs dbt Core: Feature & Cost Comparison (2026)

Honest comparison of dbt Cloud vs. dbt Core — IDE, scheduler...

dbt

dbt Project Structure: models/seeds/macros Layout (2026)

Recommended dbt project layout — models, seeds, macros, snap...

dbt

dbt_project.yml Explained: Every Config (2026)

Every dbt_project.yml setting that matters — paths, vars, ma...

Browse all dbt articles (101)
© 2026 NicheeLab All rights reserved.