One Word Decides What Exists
Your SQL is the recipe. `materialized` answers “and what happens to the food once it’s cooked” — plate it fresh every time (view), cook a batch and put it in the fridge (table), don’t even light the stove because someone else’s kitchen already will (ephemeral), or top up yesterday’s batch instead of recooking it (incremental). The recipe never changes; only what happens to its output does.
Every model in Chapter 3 compiled down to the same shape: a plain SELECT sitting in `target/compiled/`. What varied — table versus view — came from one setting: `materialized`. Change that word and dbt wraps a different DDL statement around the identical SELECT — the promise Chapter 3 made at its close. Version-specific facts below assume dbt Core 1.12.0, the current stable release as of 16 July 2026.
dbt gives you four materializations usable on any model: view, table, ephemeral, and incremental. Chapter 2 already showed half of why this matters — your first model was a view, and rebuilding it felt instant; this chapter prices what that speed costs, and when it stops being right. For a model, dbt’s own fallback when nothing is set is view; seeds and snapshots default differently.
`materialized` lives in two places: inside the model as `{{ config(materialized='table') }}`, or project-wide in `dbt_project.yml` under `models:`. Both are normal — which one wins when a model declares both is precedence, covered once you’ve seen all four materializations (Section 6).
`materialized` never touches your SQL. It decides what object exists, when the work happens, and who pays: build cost once per run, query cost once per read. Choosing a materialization is choosing which of those you want to be large — storage and staleness are consequences, not separate levers.
create or replace view
analytics.dbt_vijay.fct_subscription_revenue
as (
select
p.payment_id,
p.subscription_id,
s.listener_id,
s.plan,
p.amount_usd,
cast(p.paid_at as date) as paid_on
from raw.stripe.payments p
join analytics.dbt_vijay.stg_subs s
on s.subscription_id = p.subscription_id
where p.status = 'succeeded'
);Your SQL is the recipe. `materialized` answers “and what happens to the food once it’s cooked” — plate it fresh every time (view), cook a batch and put it in the fridge (table), don’t even light the stove because someone else’s kitchen already will (ephemeral), or top up yesterday’s batch instead of recooking it (incremental). The recipe never changes; only what happens to its output does.
Materialization is the one decision in a dbt project with a genuine, checkable cost attached at scale, and it is usually made once, at model creation, then never revisited. A model built as a table because “tables feel safe”, and never read more than once a day, burns warehouse-seconds every night for nobody; a view queried two hundred times a day by a live dashboard burns far more. Getting this one word right, model by model, is high-leverage — and it is entirely learnable from four numbers.
- Materialization
- The strategy dbt uses to persist a model’s SELECT in the warehouse, or not persist it at all. Set via `materialized`, it controls the object type, never the query logic.
- Build cost
- Warehouse time spent computing (and, if applicable, writing) a model’s rows. Paid once per dbt run, however many times the result is read afterward.
- Query cost
- Warehouse time spent whenever a model’s data is read. Paid once per read — and multiplies with every dashboard refresh, every downstream model, every ad hoc query.
- {{ config() }}
- The in-model Jinja block that sets a model’s own configuration, including `materialized`. Evaluated at compile time and gone from what reaches `target/compiled/` — Chapter 6 covers Jinja properly.
Treating `materialized` as a house style (“we always use tables here”) instead of a per-model decision. Some models pay a nightly build cost their reads never recoup; others pay a query cost on every read that one daily build would have eliminated. The four numbers this chapter prices — build, query, storage, staleness — are cheap to check and expensive to guess at.
Before setting `materialized` on a new model, ask how many times a day it is actually read — not how important it feels.
State build cost, query cost and staleness explicitly when you change a model’s materialization; “I made it a table” is not a reviewable sentence on its own.
Default new models to view. Earn table, and later incremental, with a number, not a feeling.
Every materialization is a position on the same trade-off: pay once at build time, or pay every time at query time. Freshness and storage are downstream consequences of that choice, not separate axes — keep this framing and the rest of the chapter is arithmetic.
`materialized` lives in-model as `{{ config(materialized='table') }}`, or project-wide in `dbt_project.yml` under `models:`. Both are normal; when a model has both, Section 6 works out exactly which one wins.
materialized_view exists too, but dbt-snowflake — Wavelength’s warehouse — does not support it; it uses a different mechanism, `config(materialized='dynamic_table')`, covered in a later chapter. `snapshot`, `seed`, and `test` are technically materializations as well — this chapter is about model materializations only.
`materialized` changes nothing about your SELECT and everything about what happens after it — the object that exists, when the work happens, and who pays for it.
