DBTFoundations

Materializations: View, Table, Ephemeral, Incremental

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 · Easy
01 · The one config word

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.

5 min · Topic 1 of 9

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.

Same compiled SELECT, four wrappers — pick one
target/run/wavelength_analytics/models/marts/fct_subscription_revenue.sql
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'
);
Build (paid once per run)0.4s
Query (paid once per read)34s
Stored~0 (a definition, not data)
What now exists in the warehouseA view definition only. Reading it re-runs the join and the source scan every time.Max staleness0s — relative to its inputs
Why the numbers are what they areThe 34s is paid on every read, because a view has nothing stored — it repeats the same 32s of scan-and-join, plus 2s to roll the result up, each time someone queries it.
Core mental model

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.

Why it matters

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.
Common mistake

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.

Better habit

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.

Two costs, one choice

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.

Two places, one word

`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.

A fifth materialization exists, but not in this chapter

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.

Remember this

`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.