Tables dbt Left Behind

Concepts covered: Materializations

dbt's picture of the warehouse is the project. Each run walks the models it parsed and creates or replaces 1 relation per model, named after the model. At the start of a run dbt may list the relations in the schemas it builds into, so it knows whether each name it is about to build already exists, but it acts only on the names its parsed nodes claim. A relation no node claims is never updated and never dropped. So every rename and every deletion in a dbt project leaves a relation behind, built on the day of the edit and untouched from then on.

5 edits and what they leave

Editdbt builds on the next runLeft in the warehouse
Rename `fct_orders.sql` to `fct_orders_daily.sql``fct_orders_daily``fct_orders`, frozen at its last build
Delete `int_order_payments.sql`Nothing for it`int_order_payments`
Point `fct_revenue` at a different schema`fct_revenue` in the new schema`fct_revenue` in the old one
Add `versions` to `dim_customers` with no alias`dim_customers_v1`, `dim_customers_v2``dim_customers`
Change `materialized` from view to table`fct_orders` as a tableNothing: dbt drops the view of the same name first

The last row marks the boundary. dbt owns the name it is about to build, so a view called fct_orders gives way to a table called fct_orders. The 4 rows above it change the name or the schema, and the old name belongs to nobody. The frozen fct_orders is the dangerous case. The finance dashboard reading it renders every panel. Its monthly total on June 30 equals the total shown on March 3, the morning of the rename, because the table has held the same rows since, and no error appears anywhere.

Why dbt leaves them

The schema dbt builds into is often shared. An analyst's hand-made table, a load tool's staging copy and a table another project writes can all live beside dbt's relations, and a run that dropped everything it did not recognise would destroy them. dbt's unit of work is the node, and a deleted node has no file to run and no name to act on. The cleanup is left to the engineer, who knows which of the unclaimed names were dbt's.

Finding them

SELECT
  table_schema,
  table_name,
  table_type
FROM information_schema.tables
WHERE table_schema = 'analytics'
ORDER BY table_name

The warehouse answers with what exists. dbt ls --resource-type model --resource-type seed --resource-type snapshot answers with the nodes the project builds, seeds included because each seed loads a table of its own. dbt ls prints node names, and a relation's name can differ: an alias replaces it, so fct_orders_daily builds fct_orders, and a version without an alias adds _v and its number, so version 2 of dim_customers builds dim_customers_v2. Translate each node into its relation name before comparing. A relation no translated name matches is either one dbt left behind or a table a person created by hand inside dbt's schema. Both deserve a look, and the warehouse's last-modified metadata, where it keeps any, dates the day each orphan stopped changing.

DROP TABLE IF EXISTS analytics.fct_orders

Dropping is a statement run against the warehouse, outside dbt. Run it after the readers have moved, because a dropped table takes the dashboard from stale to broken, and a stale dashboard is at least a dashboard someone can compare against. dbt clean deletes local folders such as target and touches nothing in the warehouse.

Renaming without an orphan

{{ config(alias='fct_orders') }}

select
    order_date,
    count(*) as orders,
    sum(amount) as revenue
from {{ ref('stg_orders') }}
group by
    order_date

The alias config separates the model's name in the project from the relation's name in the warehouse. fct_orders_daily.sql with alias='fct_orders' builds the relation fct_orders the dashboard already reads, so the rename is visible to every ref() and invisible to the warehouse. 2 models that would build the same relation name in the same schema stop the run at parse time with an error naming both, so an alias can never silently overwrite a neighbour. Without the alias, a rename is 3 changes in a fixed order: rename the file and build, move the dashboard to the new name, drop the old relation. Dropping first leaves the dashboard with nothing, and never dropping leaves it showing frozen numbers.

Do
  • Treat every file rename or deletion as a warehouse change with a drop step at the end
  • Use alias when the relation name must hold while the model name changes
  • List orphans with a catalog query after each round of refactoring
  • Drop a relation only after confirming nothing reads it
Avoid
  • Expect dbt run to drop anything whose model is gone
  • Trust a dashboard with no errors to be reading fresh data
  • Drop the old relation before its readers have moved

About this lesson

This section is part of the Models: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.