In dbt, what does the ephemeral materialization do to a model's compiled SQL?
answer
- nothing is created in the warehouse
- the SQL gets pasted somewhere
- think common table expression
- you cannot select from it
- inlined once per downstream consumer
basics
~20 sAn ephemeral dbt model creates no object in the warehouse. Its SQL is interpolated as a common table expression into every downstream model that refs it, so the logic is reusable in dbt but not queryable outside it.
solid answer
~50 sWith `materialized='ephemeral'`, dbt builds nothing — no view, no table. Instead, when a downstream model calls `ref()` on it, dbt inlines its compiled SQL as a CTE at the top of that model's query and rewrites the `ref` to point at the CTE name. The upside is that shared interim logic stays in one file and in the DAG without cluttering the warehouse with objects nobody queries. The downsides are real: you cannot select from it in a SQL client or a BI tool, debugging means reading the compiled SQL of downstream models, and because the CTE is pasted into each consumer, work is repeated per consumer rather than computed once. I keep ephemeral for thin logic used by one or two models, and promote anything expensive or reused widely to a view or table so it is computed once and can be inspected.
code
sql · 6 lines-- models/intermediate/int_active_customers.sql
{{ config(materialized='ephemeral') }}
select customer_id, region
from {{ ref('stg_customers') }}
where status = 'active'go deeper
Recall that ephemeral is one of the four built-in materializations and that it produces no database object, being inlined as a CTE instead.
Describe the compile-time rewrite: the model's SQL becomes a CTE prepended to each consumer, and ref resolves to that CTE alias rather than a table name.
Argue the trade-off — repeated execution per consumer versus warehouse clutter — and be able to say when you would promote an ephemeral model to a view for cost or debuggability.
Set the convention for the intermediate layer: which interim models stay hidden, which are published as views for inspectability, and how deep a chain of inlined CTEs the team will tolerate.
## What it is `ephemeral` is the fourth built-in dbt materialization, alongside `view`, `table` and `incremental`. It is the odd one out: the other three produce a database object, and ephemeral deliberately does not. ```sql -- models/intermediate/int_active_customers.sql {{ config(materialized='ephemeral') }} select customer_id, region from {{ ref('stg_customers') }} where status = 'active' ``` Running `dbt run` on this model creates nothing. The model still exists in the DAG, still has upstream dependencies, and still shows in the docs — but there is no relation with that name in the warehouse. ## How dbt compiles it When a downstream model does `{{ ref('int_active_customers') }}`, dbt does not resolve that to a schema-qualified table name. It takes the ephemeral model's compiled SQL, wraps it in a CTE, prepends it to the consumer's query, and rewrites the reference to the CTE alias: ```sql with __dbt__cte__int_active_customers as ( select customer_id, region from analytics.stg_customers where status = 'active' ) select region, count(*) from __dbt__cte__int_active_customers group by 1 ``` Chains compose: an ephemeral model that refs another ephemeral model produces two stacked CTEs. Everything is resolved at compile time and sent to the warehouse as a single statement. ## What you gain - **No warehouse clutter.** Interim logic that exists purely to be reused does not become an object analysts have to ignore, or that a BI tool auto-discovers. - **DRY logic in the DAG.** The definition lives in one versioned file with lineage, rather than being copy-pasted into three models. - **No build step.** There is nothing to create, so it adds nothing to run time on its own. ## What you give up - **You cannot query it.** No `select * from int_active_customers` in a SQL client, no BI access, no ad-hoc inspection. This is the most common source of frustration. - **Debugging is indirect.** To see what the model produces you must open the compiled SQL of a downstream consumer in `target/compiled/` and run the CTE by hand. - **Work is repeated per consumer.** If three models ref it, the CTE is inlined three times and executed three times. For heavy logic — a large aggregation, a window over a big scan — a view or table computes it once and is usually cheaper overall, and a table also lets the warehouse store and prune the result. - **Compiled SQL grows.** Deep chains of ephemeral models produce long, hard-to-read statements, and query plans become correspondingly harder to reason about. One practical consequence worth knowing: because there is no relation, an ephemeral model has no `{{ this }}` and cannot be selected as a build target the way a view or table can — running it alone does nothing observable. ## When to use it Good fits are thin, cheap, reusable filters and lookups: an active-customers filter, a currency conversion join, a small mapping used by two marts. The test is whether anyone would ever want to query it directly and whether the logic is cheap enough that re-executing it per consumer is irrelevant. Poor fits are anything expensive, anything reused by many models, and anything an analyst might want to inspect while debugging a number. In those cases a `view` gives you the same DRY reuse plus queryability, at essentially the same build cost. When people describe ephemeral as "a view with no object," that is roughly right in spirit — but a view is computed once per read from one definition, while an ephemeral CTE is pasted into and executed inside each consumer's query. ## Where it sits among the four Think of the four built-ins as answering "what should exist?": a view stores the query, a table stores the rows, an incremental model stores rows and adds new ones over time, and an ephemeral model stores nothing at all and lives only inside its consumers. Ephemeral is the lightest and the least observable, which is exactly the trade you are making.
- If ephemeral models create no objects, why not use them everywhere instead of views?Because the CTE is inlined into every consumer and executed there, so expensive logic reused by five models runs five times, and you cannot query or debug the model directly. A view gives the same single-definition reuse, stays inspectable in a SQL client, and costs essentially nothing to build.
- How do you inspect what an ephemeral dbt model actually produces?Open the compiled SQL of a model that refs it, under `target/compiled/`, and run the generated CTE by hand in a SQL client. There is no relation to select from. When this becomes a routine need it is a signal to promote the model to a view.
An ephemeral model is a phrase you copy into every letter that needs it, rather than a document you file. The wording stays consistent because it comes from one source, but there is no filed copy anyone can go and read on its own.
saying these in an interview costs you the question
- Thinking dbt creates a temporary table for ephemeral models
- Expecting to query an ephemeral model from a BI tool
- Assuming the logic runs once regardless of how many models ref it
- Using ephemeral for an expensive aggregation reused widely
- Believing an ephemeral model has a {{ this }} relation