dbt's materialization controls how a model is persisted as a database object and when it gets refreshed. Pick the wrong one and you pay for it in cost, latency, and availability.
This article walks through view / table / incremental / ephemeral — how they differ and when to use each — from both the practical engineering side and the Analytics Engineer exam perspective.
dbt lets you switch the materialized type per model. That changes the kind of database object created (or not created), the refresh granularity, and the cost/permission profile. Start by understanding the four types, then choose based on your requirements: recompute cost, latency, downstream dependencies, and permissions.
As a rough guide: use view / ephemeral for lightweight exploration and intermediate steps, table for stable serving tables, and incremental for fact tables and huge tables that need continuous incremental updates. That said, partitioning, clustering, and supported strategies (merge / append / insert_overwrite) are adapter-specific, so check your data platform's support matrix (the dbt docs and each adapter's documentation are the source of truth).
| Materialization | DB object | Refresh model | Cost / performance profile |
|---|---|---|---|
| view | VIEW | Recomputed on every query | Zero storage; compute cost on every read |
| table | TABLE | Fully rebuilt on every dbt run (default) | Uses storage; reads are fast and stable |
| incremental | TABLE | Applies only the delta (append / merge / insert_overwrite, etc.) | Avoids full recompute for major efficiency gains |
| ephemeral | None (inlined as a CTE) | Embedded into the downstream query at compile time | No extra DB object; speed depends on the optimizer |
Conceptual view of the four materializations and their dependencies
Project defaults plus per-model overrides
# dbt_project.yml (excerpt)
models:
my_project:
+materialized: view # set project default to view
marts:
+materialized: table # override to table for this subpath
# models/stg_orders.sql (per-model override)
{{ config(materialized='ephemeral') }}
select ...
# models/fct_orders.sql
{{ config(materialized='incremental', unique_key='order_id', incremental_strategy='merge') }}
select ... -- typically scope rows with is_incremental()view creates a logical view via CREATE VIEW. It uses no storage and queries flow through to the underlying tables on read. A good fit for early development and small intermediate aggregations.
The catch: stacking heavy aggregations across multiple views means upstream gets recomputed on every downstream query, slowing things down. If you need consistent serving performance, materialize as table instead. Some adapters offer options like late binding views (deferring dependency resolution), which gives different tolerance to schema evolution errors. Check the adapter docs as needed.
Minimal view model
{{ config(materialized='view') }}
select
o.id,
o.created_at,
c.country
from {{ ref('raw_orders') }} as o
left join {{ ref('dim_customers') }} as c on o.customer_id = c.idtable is built with CREATE TABLE AS SELECT on each dbt run and rebuilt every time the model runs (the default). It suits workloads that prioritize read performance and downstream stability.
If the cost of full rebuilds becomes non-trivial, consider moving to incremental. Partition/cluster settings are adapter-specific (BigQuery's partition_by/cluster_by, Snowflake clustering keys, etc.). When available they let you balance read cost against rebuild cost.
table model (cluster settings are adapter-specific)
{{ config(materialized='table') }}
select * from {{ ref('int_orders_enriched') }}incremental creates the table on the first run, then applies only the delta on subsequent runs. The delta strategy (incremental_strategy) varies by adapter and typically includes append, merge (upsert by key), and insert_overwrite (partition-level replacement). For merge, specifying unique_key is critical.
Filter the delta with the is_incremental() macro, gating on updated_at or a load watermark. To trigger a full recompute, run dbt run --full-refresh. When schema evolution happens (e.g., new columns), always check the on_schema_change setting (ignore, fail, append_new_columns, sync_all_columns, etc.; adapter support varies).
Typical merge-strategy pattern (with key-based updates)
{{ config(
materialized='incremental',
unique_key='order_id',
incremental_strategy='merge'
) }}
with src as (
select * from {{ ref('stg_orders') }}
{% if is_incremental() %}
where updated_at >= (
select coalesce(max(updated_at), '1900-01-01') from {{ this }}
)
{% endif %}
)
select * from srcephemeral creates no database object — it expands inline as a common table expression (CTE) in the downstream model's SQL. Useful when you want intermediate steps split into separate files without adding objects to the database.
The catch: cramming heavy processing into many ephemerals leads to bloated single queries that are hard for the optimizer to handle. You also can't reference them from external tools or grant on them. To inspect behavior, dbt compile and look at the generated SQL — it's the fastest path.
ephemeral model and its consumer
-- models/_int_orders_ephemeral.sql
{{ config(materialized='ephemeral') }}
select *
from {{ ref('stg_orders_raw') }}
where is_valid = true
-- models/fct_orders.sql (the consumer — ephemeral expands here as a CTE)
{{ config(materialized='table') }}
with cleaned as (
select * from {{ ref('_int_orders_ephemeral') }}
)
select * from cleanedThe exam frequently asks which materialization fits which requirement, the role of unique_key in delta strategies, what --full-refresh actually does, and the characteristics of ephemeral. In practice, the safe pattern is to start with view/ephemeral during early development, then promote to table/incremental once things stabilize.
To switch materialization by environment (dev/prod), branch config on a variable or target.name. Plan promotion and demotion deliberately, with an eye on dependency fan-out, recompute cost, and the permission model.
Example: switching materialization by environment
{{ config(
materialized= (target.name == 'prod') and 'incremental' or 'view',
unique_key= (target.name == 'prod') and 'id' or none
) }}
select * from {{ ref('stg_items') }}Analytics Engineer
問題 1
You need to update a huge fact table daily. The rows to update can be identified by updated_at, and existing rows must be overwritten. Which design best minimizes runtime and cost while preventing duplicates?
正解: A
When you need delta updates plus overwriting existing rows, incremental with the merge strategy and a unique_key is the standard answer. Filtering the delta by updated_at inside is_incremental() keeps runtime and cost down while preventing duplicates. table fully rebuilds every run (inefficient), view pays compute on every read and makes it hard to keep performance consistent, and ephemeral never produces a final serving object.
How do I switch between view in development and incremental in production?
Branch on target.name (the environment name) or vars inside config. For example: {{ config(materialized=(target.name=='prod') and 'incremental' or 'view', unique_key=(target.name=='prod') and 'id' or none) }} produces incremental in prod and view elsewhere.
What happens when a column is added to an incremental model?
Behavior depends on on_schema_change and adapter support. Options include ignore, fail, append_new_columns, and sync_all_columns, with availability varying by adapter. To apply changes reliably, use --full-refresh, or set append_new_columns / sync_all_columns when supported.
When should I choose ephemeral vs. view?
ephemeral inlines intermediate steps as CTEs and adds no database object. It fits light processing that does not need direct downstream reference or grants. view exposes the model directly and suits shared logic or access control. Use table or incremental when you need heavy processing or stable performance.
Practice with certification-focused question sets
無料で問題を解いてみるNicheeLab Editorial Team
NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.
dbt Models: SQL-Defined Transformation Units (2026)
Model fundamentals — SELECT-based definitions, naming, refs,...
dbt Analytics Engineering Exam: Complete Guide (2026)
Pass the AE Certification — scope, weighting, sample questio...
dbt Cloud vs dbt Core: Feature & Cost Comparison (2026)
Honest comparison of dbt Cloud vs. dbt Core — IDE, scheduler...
dbt Project Structure: models/seeds/macros Layout (2026)
Recommended dbt project layout — models, seeds, macros, snap...
dbt_project.yml Explained: Every Config (2026)
Every dbt_project.yml setting that matters — paths, vars, ma...