What you will be able to do
- Decide whether a change to a shared model needs a new version or can ship in place
- Declare model versions in YAML with the versions property, version identifiers and include/exclude columns
- Predict which version an unpinned ref resolves to, and pin a ref to a specific version with the v argument
Key concept
Model version — A model version is a separate implementation of the same logical model. It sits in the same repository and is deployed beside the other versions, so the producer can ship a breaking change while consumers stay on the old shape until they migrate.
1.When a change needs a new version
Other teams, dashboards and external systems query a shared final model, so it behaves like an API. Its producer needs to be able to change the logic and structure. Its consumers need their queries to keep working without warning. Model versions handle that conflict directly. The breaking shape lives in a new version, and the old one stays queryable during a migration window.
Ordinary changes do not need versioning. Version control already deploys the latest state of the main branch, and you can roll back by reverting a commit. Versions are for the case where you cannot migrate every consumer yourself: people, systems and processes beyond your team's control depend on the model. For smaller organisations, or for models that are still changing fast, it is often fine to make every consumer handle a breaking change as soon as it reaches production.
A [model contract](lesson:2.1) is what makes a breaking change visible. Removing or renaming a column, or changing its data type, breaks the guarantees the contract made. When you make that change on purpose, it belongs in a new version. Additive changes and bug fixes do not need one. A third group of changes keeps the column names and types but can still change what consumers see, such as changing granularity or rewriting CASE logic. For those, the model owner has to judge whether consumers would be surprised.
| Change | Classification |
|---|---|
| Removing a column | Breaking: create a new version |
| Renaming a column | Breaking: create a new version |
| Changing a column type | Breaking: create a new version |
| Adding a new column | Non-breaking: change in place |
| Fixing a bug in an existing column | Non-breaking: change in place |
| Changing metric logic, granularity, filters or CASE statements | Might be breaking, depending on business logic |
Each version adds coordination work and puts one more relation in the warehouse. So prefer non-breaking changes, and let unused columns build up. Then cut a new latest version on a predictable cadence, about once or twice a year and announced well in advance, and drop the dead columns at that point. Don't release a version for every small change. Usually only two or three versions are live at a time.
Checkpoint 1 of 5· Check yourself
Your contracted fct_orders model is used by three other teams. You need to add a discount_amount column and fix a rounding bug in order_total. What should you do?
Adding a column and fixing a bug in an existing column are both non-breaking, so neither needs a new version.
“If you're making a non-breaking change, you don't need a new version—such as adding a new column”Source: docs.getdbt.com
Sources1
2.Declaring versions in YAML
You declare versions in the model's property file with a versions list. Each entry needs a v, the version identifier. The identifier can be an integer, a float or any string, and dbt uses it to order the versions. The recommended scheme is simple major versioning (1, 2, 3), where each number marks a breaking change. Write 2, not v2, because dbt adds the letter itself. If the identifiers are not all numeric, dbt sorts them alphabetically.
models:
- name: model_name
versions:
- v: <version_identifier> # required
defined_in: <file_name> # optional -- default is <model_name>_v<v>
columns:
# specify all columns, or include/exclude columns from the top-level model YAML definition
- include: <include_value>
exclude: <exclude_list>
# specify additional columns
- name: <column_name> # required
- v: ...
# optional
latest_version: <version_identifier>Names follow the <model_name>_v<v> convention. For version 2 of fishtown_analytics_orders, dbt looks for the SQL in fishtown_analytics_orders_v2.sql and materializes it under the alias fishtown_analytics_orders_v2 by default. Every version needs its own SQL file. The latest version may instead live in a file with the plain base name. defined_in points a version at a different file, and a custom alias overrides the relation name. dbt does not keep those two settings in sync, so use them only when you have a good reason. File names also have to be unique across the whole project.
Columns are declared once at the top level, and each version describes how it differs. If a version declares no columns, it inherits all of them. include: "*" with an exclude list drops specific columns. A named column inside the version overrides the top-level column of the same name. In the docs' customers example, v4 takes every top-level column. v3 keeps them all but redefines customer_country. v2 excludes customer_country. v1 uses include: [] and declares only an integer id.
Checkpoint 2 of 5· Match them up
Match each version entry from the customers example to the columns it ends up with
Tap a term, then the definition that fits it.
A version with no columns inherits the top level. An exclude list removes columns. A named column overrides the inherited one. An empty include list starts from nothing.
“v2 will include all columns except customer_country.”Source: docs.getdbt.com
Sources2
3.Latest version and pinned refs
latest_version controls where unpinned references go. A ref() with no version resolves to the latest version. When the key is missing, the latest version is the numerically greatest identifier, or the alphabetically last one if the identifiers are strings. This default can catch you out: merge a v2 file without setting latest_version, and every unpinned consumer moves to v2 on the next run. While a new version is still being tested, set latest_version explicitly to the old one.
models:
- name: model_name
latest_version: 2
versions:
- v: 3
- v: 2
- v: 1Every version therefore has one of three roles. The latest version is the one unpinned refs resolve to. A higher version is prerelease, and a lower one is old. The version: selection method selects models by these roles. A consumer who wants a particular version passes the optional version (or v) argument to ref, for example {{ ref('upstream_project', 'fishtown_analytics_orders', v=2) }}. A pinned ref stays on that version even when latest_version changes. That is how consumers keep working through a migration window, and how they try out a prerelease version early.
Checkpoint 3 of 5· Fill the gap
Which key makes unpinned refs resolve to v2 even though v3 exists?
models:
- name: model_name
? : 2
versions:
- v: 3
- v: 2
- v: 1latest_version must name one of the declared identifiers, and it decides where unpinned ref() calls resolve.
Source: docs.getdbt.comCheckpoint 4 of 5· Check yourself
Given latest_version: 2 with versions 1, 2 and 3 declared, what is v3?
A version above latest_version is prerelease, and one below it is old. Prerelease versions are still built and can be referenced with v=3.
“v3 is considered "prerelease", and v1 is considered "old".”Source: docs.getdbt.com
Checkpoint 5 of 5· Exam question
`fct_orders` is a contracted model queried directly by three downstream teams. Analytics engineering needs to drop a deprecated column but cannot force every consumer to migrate on the same day. Which approach removes the column without an immediate breaking change for existing consumers?
Correct answer: A — Add a new version that excludes the column via `include: all` plus `exclude`, keep `latest_version` on the current version until consumers migrate, then set a `deprecation_date` on it.
- A. Creating a new version that drops the column, while keeping latest_version pointed at the current version and adding a deprecation_date to it, gives consumers a defined migration window with no immediate breakage.
- B. Deleting the column outright makes the model's SQL output mismatch its enforced contract, so the very next build fails immediately instead of giving consumers time to react.
- C. Renaming the source column and adding a failing test breaks every consumer on their next run rather than providing a graceful migration period.
- D. Moving the column to a separate model still requires every consumer that selects it today to rewrite their query to reference the new model, which is itself an immediate breaking change.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.Adding a v2 file to a versioned model is safe because existing unpinned refs stay on v1 until you change them.Why is that wrong?
When latest_version is not set, unpinned refs resolve to the highest version, so adding v2 moves every unpinned consumer at once. Set latest_version to 1 until you are ready to switch.
Covered in Latest version and pinned refs
2.Any change to a contracted model, including a new column, needs a new model version.Why is that wrong?
Only intentional breaking changes, such as removing or renaming a column or changing its type, need a new version. Adding columns and fixing bugs are changed in place.
Covered in When a change needs a new version
3.Version identifiers should be written as v1, v2 so the file name matches.Why is that wrong?
Write a bare identifier such as 2. dbt adds the v itself when it builds the file name and alias, giving names like fishtown_analytics_orders_v2.
Covered in Declaring versions in YAML
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“Defining model versions is appropriate when people, systems, and processes beyond your team's control, inside or outside of dbt, depend on your models.”
↩︎ When a change needs a new version“opt for a predictable cadence (once or twice a year, communicated well in advance)”
↩︎ When a change needs a new version“During that migration window, anywhere that model is being used downstream, it can continue to be referenced at a specific version.”
↩︎ Latest version and pinned refs“Multiple versions of a model will live in the same code repository at the same time”
↩︎ Key concept“If you're making a non-breaking change, you don't need a new version—such as adding a new column”
↩︎ Checkpoint - 2.
“The standard convention for naming model versions is <model_name>_v<v>.”
↩︎ Declaring versions in YAML“Model file names must be globally unique, even when defining versioned implementations of a model with a different name.”
↩︎ Declaring versions in YAML“You should not include the letter v in the version identifier, as dbt will do that for you.”
↩︎ Exam trap 3“v2 will include all columns except customer_country.”
↩︎ Checkpoint - 3.
“The ref function supports an optional keyword argument - version (or v).”
↩︎ Latest version and pinned refs
Also cited
“Without setting this, the default will be the highest numerical version, which could be a breaking change for consumers.”
↩︎ Exam trap 1“If a change breaks the contract, like removing or changing a column type, create a new model version with an updated contract”
↩︎ Exam trap 2“If not specified for a versioned model, latest_version defaults to the largest version identifier”
↩︎ Prediction“v3 is considered "prerelease", and v1 is considered "old".”
↩︎ Checkpoint