DBTStructure & Production

Performance and Cost

How analytics engineers turn raw warehouse tables into trusted models — pick a topic on the left and its full breakdown loads here: the mental model, the compiled SQL dbt actually issues, live runs you can drive yourself, and the failure modes that quietly ship wrong numbers.

18 min readTopics chapter readerLevel · Hard
01 · Attribution comes first

The Invoice With No Names

A warehouse bill is an aggregate over queries you cannot individually see. `query_tag` (Snowflake) or `labels` (BigQuery) do not make anything cheaper — they make the aggregate legible, by writing a model name onto each query before it runs. Attribution is not an optimisation; it is the precondition for one.

4 min · Topic 1 of 10

Wavelength’s Snowflake warehouse bills $114.60 a month for the nightly `dbt build`. Somewhere in that number is one model responsible for 60% of it. Open the invoice below and try to find it.

Commit to a guess before the itemisation appears — which of these six models is the biggest line?

Commit before the invoice opens — the options lock the moment you doWavelength's Snowflake warehouse (Large, Enterprise, $3.00/credit) bills $114.60/month for the nightly `dbt build`. Which one of these six models is the biggest line item on it?
Core mental model

A warehouse bill is an aggregate over queries you cannot individually see. `query_tag` (Snowflake) or `labels` (BigQuery) do not make anything cheaper — they make the aggregate legible, by writing a model name onto each query before it runs. Attribution is not an optimisation; it is the precondition for one.

Why it matters

Without a tag, a warehouse bill and a dbt project are two documents that do not talk to each other. Snowflake bills the WAREHOUSE — size times time running — never the query, so QUERY_HISTORY has no model column at all until you put one there yourself. `query_tag` (a project- or model-level config; dbt runs `ALTER SESSION SET QUERY_TAG` before the model and resets after) is how a dbt run’s SQL gets a name attached in the warehouse’s own accounting. Skip it, and every cost conversation in this chapter — every widget past this one — is unavailable to you in production, because the raw bill genuinely does not name its own biggest contributor.

query_tag (Snowflake)
Project-level `+query_tag:` or `{{ config(query_tag='...') }}`. dbt’s own docs flag a caveat: a mid-materialization failure can leave a SUBSEQUENT query running under the wrong tag — which matters precisely for cost attribution.
labels (BigQuery)
The config key is `labels`, a dict — NOT `query_label`. `+labels:` in dbt_project.yml or `{{ config(labels={...}) }}`. Values over 63 characters are silently truncated by BigQuery.
Databricks: automatic since 1.11
dbt-databricks 1.11+ auto-tags every query with `@@dbt_model_name`, `@@dbt_materialized`, `@@dbt_core_version` and `@@dbt_databricks_version`, queryable in `system.query.history` — zero config. Recently landed modern practice (Databricks blog, published 2026-07-01), not folklore.
run_results.json is not query_history
`execution_time` (schema v6, dbt Core 1.12) is dbt’s WALL CLOCK per node — not warehouse cost. A node that took 300s because it queued behind a saturated warehouse is indistinguishable in this file from one that burned 300s of compute. Separating them needs `query_history` or `INFORMATION_SCHEMA.JOBS`.
Attribution, by warehouse
WarehouseConfig keyWhere it shows upThe catch
Snowflake`query_tag`QUERY_HISTORY.QUERY_TAGA failed mid-run query can leave the NEXT one mistagged
BigQuery`labels` (a dict, not `query_label`)INFORMATION_SCHEMA.JOBS labelsTruncated silently past 63 characters
Databricks (1.11+)none — automatic`system.query.history`Only as current as your dbt-databricks version

None of these three make a query cheaper. All three make the bill legible enough to ask which model is expensive in the first place.

Common mistake

Assuming BigQuery’s attribution config is `query_label`, by analogy with Snowflake’s `query_tag`. The real key is `labels`, a dict. A model configured with a nonexistent key silently does nothing — no error, no warning — and every query still lands untagged in `INFORMATION_SCHEMA.JOBS`.

Shipping `query_tag` only after a cost review is already underway. Every query that ran before the tag existed is permanently untagged in `query_history`. You can price this month forward; last month’s invoice stays a document with no model names in it, forever.

Better habit

Ship `query_tag` (or `labels`) in the same pull request as the model, not the pull request that adds cost monitoring — by then, months of history are already unattributed.

Read `query_history` before opening a cost calculator. A projected number is a guess; a tagged, queried number is a fact.

On Databricks 1.11+, verify the auto-tags actually appear in `system.query.history` once, rather than assuming the version bump alone did it.

Provenance of every number in this chapter

None of the timings, transcripts or dollar figures below were captured from a live dbt Core 1.12.0 run against a real warehouse. Every second comes from this KB’s own published Wavelength run profile, run through a real discrete-event scheduler in the widgets — not a lookup table — so the reader’s own thread dial recomputes it the same way dbt’s scheduler would. Every dollar figure is computed from a cited, dated public rate card, with the rate itself editable in the calculators. Chapter 8, 10 and 11 carried the same note, for the same reason.

The version this chapter targets

dbt Core 1.12.0, released 16 July 2026. Every transcript below uses this KB’s canonical production clock, 2026-07-25 06:00 UTC. Where the newer engine changes anything, it gets exactly one sentence, later in this chapter — Chapter 11 already handed its full story to Chapter 22.

"How would you find the most expensive model in a dbt project?"

The weak answer opens `run_results.json` and looks for the biggest `execution_time`. The strong answer starts one step earlier: without `query_tag` or `labels`, the warehouse bill has no model column at all, so the honest first move is checking whether attribution is even configured — and if it is not, saying so is the correct senior answer, not a workaround.

Remember this

A warehouse bill has no model names until `query_tag` or `labels` puts them there — attribution is the precondition for every other question in this chapter, not an optional refinement.