CertSafari
    DBT Analytics Engineering· Lessons

    Domain 2 · Lesson 16/31

    Creating model versions in dbt

    Creating different versions of our models and deprecating the old ones

    10 min read
    3.23% of exam
    5 sources
    Published 12 Oct 2026
    Docs as of 12 Oct 2026

    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.

    How the producer guide classifies common changes to a contracted model
    ChangeClassification
    Removing a columnBreaking: create a new version
    Renaming a columnBreaking: create a new version
    Changing a column typeBreaking: create a new version
    Adding a new columnNon-breaking: change in place
    Fixing a bug in an existing columnNon-breaking: change in place
    Changing metric logic, granularity, filters or CASE statementsMight 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?

    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.

    Skeleton of the versions property, showing the optional defined_in, columns and latest_version keysyaml
    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.

    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.

    latest_version pinned below the highest version: v3 is prerelease, v2 is latest, v1 is oldyaml
    models:
      - name: model_name
        latest_version: 2
        versions:
          - v: 3
          - v: 2
          - v: 1

    Every 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: 1

    Checkpoint 4 of 5· Check yourself

    Given latest_version: 2 with versions 1, 2 and 3 declared, what is v3?

    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?

    Sources31

    Exam traps

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

    1. 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. 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. 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. 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. 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. 3.
      “The ref function supports an optional keyword argument - version (or v).”
      ↩︎ Latest version and pinned refs

    Also cited

    Continue to page 2 of 2

    Deprecating and removing old dbt model versions

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