Ephemeral Models

Concepts covered: Materializations

A model dbt never builds

Materialized as a view or a table, a model becomes an object in the warehouse. Materialized as ephemeral, it becomes nothing there. dbt keeps its SQL, and when another model refs it, dbt pastes that SQL into the referencing model as a CTE and sends the combined statement. The ephemeral model exists in the project files and in the lineage, and never in the schema. Here the intermediate model that section 1 asked for settles subscriptions to 1 row per user.

{{ config(materialized='ephemeral') }}

select
    user_id,
    max_by(plan_code, started_at) as plan_code
from {{ ref('stg_billing__subscriptions') }}
where status = 'active'
group by
    user_id

The group by settles the grain to 1 row per user whatever the subscriptions table holds, and max_by returns the plan_code of the active row that started most recently. The mart from section 0 now refs this model instead of the staging view.

select
    d.user_id,
    d.play_date,
    c.plan_code,
    d.plays,
    d.minutes_played
from {{ ref('int_plays__daily_by_user') }} as d
left join {{ ref('int_subscriptions__current') }} as c
    on d.user_id = c.user_id

The SQL dbt sends

create table analytics.fct_user_daily_listening as
with __dbt__cte__int_subscriptions__current as (
    select
        user_id,
        max_by(plan_code, started_at) as plan_code
    from analytics.stg_billing__subscriptions
    where status = 'active'
    group by
        user_id
)
select
    d.user_id,
    d.play_date,
    c.plan_code,
    d.plays,
    d.minutes_played
from analytics.int_plays__daily_by_user as d
left join __dbt__cte__int_subscriptions__current as c
    on d.user_id = c.user_id

The ref() to int_subscriptions__current became the CTE name __dbt__cte__int_subscriptions__current, and the whole body of the ephemeral model was pasted in above the mart's select. The ref() to int_plays__daily_by_user, a view, became a relation name as before. dbt sends 1 statement for the mart, and the warehouse runs the group by over subscriptions as part of building the table. When the run finishes, nothing named int_subscriptions__current exists in the schema.

MaterializationWhat dbt sends for the nodeWhat the warehouse holds afterWhat a ref() to it becomes
viewcreate viewa saved querythe view's name
tablecreate table asthe rowsthe table's name
ephemeralnothing of its ownnothinga CTE pasted into the caller

What that changes

Ephemeral or a warehouse object

Ephemeral fits
  • Logic read by 1 or 2 models
  • A filter or a small aggregation too light to deserve a warehouse object
  • A schema you want to show only the tables analysts should open
Make it a view or a table
  • Logic refd by many models, which would each recompute it
  • Rows you need to inspect while debugging
  • Heavy work, where a table stores the result once per build

The schema listing is the first reason. A project with 30 intermediate models as views lists 30 objects beside the 8 marts analysts want, and every analyst learns to skip past them. Ephemeral keeps the project's structure in the files and the warehouse's listing to the results. The cost is repetition: a heavy intermediate model refd by 6 marts recomputes 6 times per build, where a view or a table pays once.

Switching a model between view and ephemeral changes no SQL inside it and no SQL in any model that refs it. The 1 line of config decides whether dbt sends a create view during the run and a relation name in the mart, or nothing during the run and a CTE in the mart. Because every neighbor reaches the node through ref(), none of them depends on how the node is materialized.

About this lesson

This section is part of the Models: Intermediate 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.