Introduction

Changing a dbt model is easy until people start depending on it.

At first, it’s just your model. You rename a column, remove a field, adjust the logic, run the build, and move on.

Then the model lands in production and, slowly, it stops being just yours. A dashboard expects the old column name. A downstream model depends on the current shape. Someone from another team has built logic on top of it and probably forgot to tell you. Happens.

At that point, even a reasonable cleanup can become a breaking change.

And breaking changes are about migration paths, timing, ownership, and giving consumers enough room to move safely.

That is the real reason to care about dbt model versioning. It gives mature dbt projects a way to evolve production models without forcing every downstream consumer to migrate at once, or breaking them when they inevitably don’t.

And the nice thing is that versioning isn’t just “add _v2 to the file name and hope for the best.” Done well, it gives you a migration path: keep the stable version, introduce the new one, make the change visible, and give people time to move.

So before we get into the YAML, let’s talk about the real problem: the blast radius.

When a model change gets a blast radius

Let’s say you have a model called youtube_artists.

It’s already in production. Dashboards use it. Other dbt models reference it. Maybe a few people outside the data team query it directly, because of course they do.

Now you want to rename artist to artist_name. You also want to remove a column that no longer makes sense, for example youtube_playlist_reach.

From the model owner’s perspective, this may be a perfectly reasonable cleanup. From the consumer’s perspective, it may be a broken dashboard, a failed downstream build, or a Slack message that starts with “hey, did something change in the data?”

You have a few options, and none of the naive ones are great.

You can change the existing model directly and hope everyone is ready. Brave, but not exactly governance.

You can postpone the change until every consumer has migrated. Safe, but often painfully slow.

You can create a separate model with a new name and ask people to switch. That works for a while, until nobody remembers which one is the “real” one.

Model versioning gives you a cleaner path: keep the stable version available, release the new version next to it, and give consumers a migration window.

That is the core idea. Versioning is about making change survivable.

What dbt versioning gives you

In dbt, a versioned model lets you have multiple versions of the same logical model.

You might have:

youtube_artists_v1.sql
youtube_artists_v2.sql

with both versions configured under the same model name in YAML.

The useful part is that dbt understands these as versions of one model, not just two unrelated relations with similar names. That means you can define which version is currently considered latest, reference a specific version when needed, and manage deprecation more explicitly.

A simplified config may look like this:

models:

models:
  - name: youtube_artists
    latest_version: 1
    versions:
      - v: 1
      - v: 2

That latest_version line matters more than it looks.

If you don’t set it, dbt treats the highest version number as the latest. So once v2 exists, an unpinned ref('youtube_artists') may start resolving to v2.

Sometimes that is what you want. During a controlled migration, it often isn’t.

If v2 is still being validated, you probably want the default reference to keep pointing to v1. Consumers keep working. The new version can be tested with real data. Nobody has to pretend the migration is instant.

Small config. Big difference. Classic.

Versioning lets you release change without forcing everyone to move today

This is where dbt model versioning becomes more than a naming convention.

You can introduce v2 with the new schema, while v1 remains available for existing consumers. In our example, v2 can rename artist to artist_name and remove youtube_playlist_reach, while v1 continues to serve the old shape.

That gives you a much better migration story.

Downstream models can keep using the stable version until they are ready. Consumers who want to test the new logic can explicitly point to v2. CI can also be configured to build and validate the new version before it becomes the default.

dbt can also warn you when downstream models use an unpinned reference while a newer version is available. That doesn’t necessarily mean anything is broken. It shows you which models depend on whatever is currently marked as latest, and which ones may be affected when that changes.

This is also where pinned references become useful.

{{ ref('youtube_artists', version=1) }}

or:

{{ ref('youtube_artists', version=2) }}

In a small project, pinning may feel like an extra ceremony. In a larger one, it can be the difference between “this model depends on whatever is latest” and “this model expects this specific interface.”

And when you are dealing with breaking changes, that explicitness isn’t a bad thing.

The boring YAML details are part of the safety

There is some configuration involved, obviously. This is dbt. There will be YAML.

Each version can have its own file, columns, and configuration. That is useful when the schema changes, but also when the operational behavior changes.

If the existing production model doesn’t yet use the _v1 naming convention, dbt also lets you introduce versioning without immediately renaming the underlying file or relation. The defined_in setting points dbt to the existing model file, while alias preserves the current database object name. This can make the first step into versioning less disruptive.

For example, v1 can stay materialized as a view, while v2 is materialized as a table. Or v2 can have a different set of columns, tests, tags, or access settings.

If most columns stay the same, you don’t have to duplicate the whole column list. You can include existing columns and exclude removed/renamed and add new ones.

versions:
  - v: 2
    columns:
      - include: all
        exclude:
          - artist
          - youtube_playlist_reach
      - name: artist_name
        description: Name of the artist

That may look like a small convenience, but in real projects it matters. Long column lists are easy to duplicate badly. Documentation drifts. Tests drift. People copy things and then forget why.

Versioning works best when the config makes the change visible: this column was removed, this one was renamed, this version has a different contract.

Not “here is another model that sort of looks similar.” More like: “here is the next version of the same interface, and here is what changed.”

Deprecation is where versioning becomes governance

Creating v2 is only half of the job. You also need a plan for v1.

Otherwise, old versions stay forever because everyone is afraid to delete them. Congratulations, you now have a data museum.

dbt lets you set a deprecation_date for an older version:

versions:
  - v: 1
    deprecation_date: 2026-12-01
  - v: 2

This doesn’t automatically remove the old version when the date passes. And honestly, good.

You probably don’t want a tool deleting production dependencies just because a date in YAML expired. dbt will warn you, but the team still has to do the human part: communicate the change, migrate dashboards, update downstream models, and remove the old version when it’s actually safe.

That is the governance value.

A deprecation date gives the migration a shape. It tells consumers: the old version still exists, but not forever. Here is the window. Please move.

Versioning gives you the technical mechanism. Deprecation gives you the lifecycle.

You need both.

A stable name helps consumers stay sane

One practical issue with model versions is naming.

Developers may be perfectly happy with:

youtube_artists_v1
youtube_artists_v2

But external consumers often want one stable relation name:

youtube_artists

They don’t necessarily care which version is underneath. They want the current stable interface.

A pattern recommended in dbt materials is to create or update a canonical "pointer" view. The versioned models keep their explicit names, but dbt also maintains a view with the base model name that points to the current latest version.

So youtube_artists can point to youtube_artists_v1 today, and later to youtube_artists_v2 when the migration is complete.

If you are using new, shiny dbt 1.12 you can just add a pointer view configuration code. 

models:
  - name: youtube_artists
    latest_version: 1
    config:
      latest_version_pointer:
        enabled: true
    versions:
      - v: 1
      - v: 2

For older versions, you can achieve the same result using custom macro.

This avoids the classic “which table should I use?” problem. You know the one. It starts as a naming detail and somehow becomes a recurring meeting topic.

A canonical view gives consumers a stable entry point, while the team keeps versioned objects underneath.

Clean enough. Practical enough. That is usually the sweet spot.

Tests, CI, and contracts still matter

Model versioning makes breaking changes safer.

However, if v2 removes a column and a downstream model explicitly points to v2, that downstream model can still fail. If tests aren’t updated for the new version, they can fail for boring reasons. If documentation still describes the old schema, people will be confused. No feature saves you from confused people.

So the new version needs its own validation.

dbt selectors can help here too. You can target a prerelease version using dbt selector: version:prerelease. This way you can execute any dbt command like build or test against v2 version only.

Unit tests can target a specific version as well, so you can validate new SQL modeling logic before promoting it.

Recommended reading

dbt Tests in Action

Explore how dbt data tests, freshness checks, model contracts, and unit tests work together to protect production data pipelines.

In CI, build and test v2 while it’s still prerelease; promote it to latest only after those checks pass.

For stronger guarantees about model shape across versions, use dbt contracts.

Versioning and contracts solve related but different problems.

Versioning gives you a way to manage multiple versions and migration windows. Contracts help enforce the expected structure of a model: columns, data types, constraints.

Together, they move dbt models closer to proper interfaces. And that is a point.

Recommended reading

When to Use dbt Model Contracts

Learn when to use dbt model contracts and how they can prevent unexpected schema changes from reaching production.

When model versioning is worth it

You don’t need to version every model.

If a model is experimental, internal, or barely used, versioning may be overhead. Normal code review, tests, and communication are probably enough.

Versioning becomes worth it when changing a model requires coordination.

Use it when a model is already in production, has real downstream consumers, and a change would rename, remove, or significantly alter fields. Use it when other teams need time to migrate. Or when you want to test a new version with production data before making it the default.

Version the models whose schemas function as interfaces for other consumers.

Not every staging model needs this treatment. But a model powering dashboards, downstream marts, data products, or external consumers probably deserves more care than “merge and see what happens.”

The trade-off

There is a cost.

Versioning adds YAML. It adds files. It adds lifecycle decisions. Someone has to decide what “latest” means, when deprecation starts, when old versions are removed, and who needs to migrate.

If nobody owns that process, versioning can become another layer of clutter.

So the goal isn’t to introduce model versions because they sound mature. The goal is to use them when the cost of breaking consumers is higher than the cost of managing the migration properly.

That is the trade-off.

For small projects, it may be overkill. For mature dbt projects with shared models and downstream dependencies, it can be the difference between controlled evolution and surprise breakage.

Final thoughts

dbt model versioning is easy to frame as a technical feature: versioned files, YAML config, latest_version, pinned refs, deprecation dates.

But the more interesting story is change management.

Production data models change. Consumers don’t all migrate at the same time. Dashboards, downstream models, and other teams need stable interfaces, or at least a clear warning when those interfaces are about to change.

Versioning gives teams a practical way to handle that.

Keep the old version stable. Release the new one. Test it. Give consumers a migration window. Deprecate the old version deliberately. Remove it when the project is ready, not when someone gets brave on a Friday afternoon. Experienced dbt consulting can help teams design these processes and evolve their dbt projects without disrupting downstream consumers.

If your dbt project is mature enough that a column rename can break someone else’s day, model versioning is worth a serious look.

At that point, you are not just editing SQL anymore. You are managing a production interface.