Learning Platform
Глоссарий Troubleshooting
Урок 07.03 · 15 мин
Начальный
ephemeralCTEinlinedeep nesting

Третий тип материализации, который должен знать junior — ephemeral. Это специфичный для dbt концепт: модель, которая существует только в коде, не материализуется в warehouse, а вместо этого инлайнится как CTE в downstream-моделях.

Ephemeral — мощный инструмент, но с серьёзными подводными камнями. Разбираемся, когда применять, когда нет, и почему deep nesting убивает производительность.

Что физически делает ephemeral

В отличие от view и table, ephemeral-модель не появляется в warehouse. Никакой CREATE VIEW или CREATE TABLE не выполняется. Вместо этого dbt при компиляции downstream-моделей берёт SQL ephemeral и встраивает его как Common Table Expression в начало запроса.

Что происходит с ephemeral при компиляции

Ephemeral не создаётся в warehouse. dbt при компиляции downstream-моделей берёт SQL ephemeral и встраивает его как WITH-блок (CTE) в начало запроса. Можно сказать, ephemeral 'испаряется' в downstream SQL.

int_orders_filtered.sqlconfig(materialized='ephemeral')
dbt runничего не отправляет в warehouse
downstream uses ref('int_orders_filtered')mart модель
Компилированный SQL downstreamWITH int_orders_filtered AS (SELECT...) SELECT FROM int_orders_filtered

Из-за этого dbt run --select <ephemeral_model> ничего не делает в warehouse. Если ты запустишь это, увидишь сообщение типа “ephemeral model скомпилирован”. Но в DuckDB его нет — он существует только в скомпилированных файлах downstream-моделей.

Когда ephemeral полезен

Главный сценарий: приватный промежуточный шаг, который нужен только одной-двум downstream-моделям, и нет смысла материализовать его как отдельный объект в warehouse.

Пример: фильтрация sensitive data перед агрегацией.

-- models/intermediate/int_orders_filtered.sql
{{ config(materialized='ephemeral') }}

SELECT *
FROM {{ ref('stg_orders') }}
WHERE NOT is_test_order
  AND status != 'cancelled'

Эта модель — просто фильтр. Если её материализовать как view, downstream-запрос всё равно раскроет её при выполнении. Если как table — лишняя физическая копия данных. Ephemeral — компромисс: SQL встраивается inline, никаких объектов в warehouse, и не загромождает schema.

Когда ephemeral хороший выбор:

  • Промежуточная фильтрация / переименование, читается одной моделью.
  • Helper-расчёты, которые логически приватные (например, расчёт фискального года, который потом используется в одной mart).
  • Замена subquery — если хочется выделить кусок логики в отдельный файл для читабельности.
  • Маленькая трансформация над staging, используемая 1-2 downstream.

Когда ephemeral плохой выбор

1. Многократное использование. Если ephemeral используется в 3+ downstream-моделях, SQL будет inlined в каждую — это значит, что warehouse будет повторять одни и те же вычисления столько раз, сколько обращений. На больших данных это убивает performance. Лучше view (хотя бы план кэшируется) или table.

2. Тяжёлые aggregations. Ephemeral с GROUP BY на миллион строк, используемый в трёх mart-моделях — это три GROUP BY вместо одного. Здесь нужен либо table, либо хотя бы view.

3. Тесты на модели. Ephemeral-модель нельзя протестировать обычными тестами, потому что её нет в warehouse. not_null, unique и пр. на ephemeral не работают — dbt пропустит их или выдаст ошибку. Если на модели должны быть тесты — это не ephemeral.

4. Deep nesting (вложенность 3+). Когда ephemeral A -> ephemeral B -> ephemeral C -> final mart, скомпилированный SQL mart-а превращается в монстра-CTE. Warehouse начинает плохо его планировать. Это самая частая ловушка.

WARNING

Никогда не строй цепочки ephemeral -> ephemeral -> ephemeral. Каждый уровень добавляет nesting. На 4 уровнях query становится несколько килобайт SQL, query planner тратит на него секунды. Обнаружишь это только когда дашборды начнут тормозить.

Deep nesting в действии

Чтобы понять проблему, рассмотрим конкретный пример. Допустим, у нас цепочка:

stg_orders -> int_filter (ephemeral) -> int_join (ephemeral) -> int_aggregate (ephemeral) -> mart_revenue (table)

SQL каждой модели:

-- int_filter.sql (ephemeral)
SELECT * FROM {{ ref('stg_orders') }} WHERE NOT is_test

-- int_join.sql (ephemeral)
SELECT o.*, c.country FROM {{ ref('int_filter') }} o JOIN {{ ref('stg_customers') }} c ON ...

-- int_aggregate.sql (ephemeral)
SELECT country, SUM(amount) AS revenue FROM {{ ref('int_join') }} GROUP BY country

-- mart_revenue.sql (table)
SELECT * FROM {{ ref('int_aggregate') }} ORDER BY revenue DESC

Финальный скомпилированный SQL для mart_revenue:

CREATE OR REPLACE TABLE jaffle_shop.main.mart_revenue AS (
    WITH int_filter AS (
        SELECT * FROM jaffle_shop.main.stg_orders WHERE NOT is_test
    ),
    int_join AS (
        SELECT o.*, c.country
        FROM int_filter o
        JOIN jaffle_shop.main.stg_customers c ON o.customer_id = c.id
    ),
    int_aggregate AS (
        SELECT country, SUM(amount) AS revenue FROM int_join GROUP BY country
    )
    SELECT * FROM int_aggregate ORDER BY revenue DESC
);

На трёх уровнях это ещё нормально. На пяти — становится 200 строк CTE с непонятным флоу. На семи — отладка через target/compiled превращается в детектив.

Deep nesting проблема

Каждый ephemeral в downstream добавляет CTE-блок к финальному SQL. На 2-3 уровнях это нормально. На 5+ финальный SQL становится массивным, query planner тратит время на оптимизацию, отладка сложна.

1 уровень ephemeralOK, WITH + SELECT
2-3 уровняприемлемо, читается
4-5 уровнейWARNING: финальный SQL > 100 строк
6+ уровнейANTI-PATTERN: query planner перегружается

Решение для deep nesting — материализовать промежуточный слой как table или view. Это разрывает цепочку: следующая модель читает уже из готового warehouse-объекта, не наследует все CTE.

Тесты на ephemeral не работают

Очень тонкий момент: обычные тесты не работают на ephemeral-моделях. Когда dbt пытается выполнить not_null-тест, он генерирует SQL вроде:

SELECT COUNT(*) FROM <model> WHERE <column> IS NULL

Подставляет имя модели. Но ephemeral-модели в warehouse нет, имя не существует. dbt в большинстве версий просто пропустит тест молча (или выдаст warning).

Это значит: если на модель должны быть data-quality тесты — она не должна быть ephemeral. Используй view (минимальный overhead, тесты работают) или table.

# models/_models.yml
version: 2
models:
  - name: int_orders_filtered  # это ephemeral
    columns:
      - name: order_id
        tests:
          - not_null  # ЭТО НЕ ЗАПУСТИТСЯ

Тест в YAML будет, но запускаться не будет. Junior’ы попадают на это часто.

Ephemeral как замена subquery

Один из законных use case’ов — выделение complexity в отдельный файл для читабельности. Сравни:

-- БЕЗ ephemeral: subquery в mart
SELECT
    country,
    SUM(amount) AS revenue
FROM (
    SELECT
        o.amount,
        c.country
    FROM {{ ref('stg_orders') }} o
    JOIN {{ ref('stg_customers') }} c ON o.customer_id = c.id
    WHERE o.status != 'cancelled'
) sub
GROUP BY country
-- С ephemeral: логика вынесена
-- int_orders_joined.sql (ephemeral)
SELECT o.amount, c.country
FROM {{ ref('stg_orders') }} o
JOIN {{ ref('stg_customers') }} c ON o.customer_id = c.id
WHERE o.status != 'cancelled'

-- mart_revenue.sql (table)
SELECT country, SUM(amount) AS revenue
FROM {{ ref('int_orders_joined') }}
GROUP BY country

Второй вариант читабельнее: каждый шаг изолирован, можно ref на int_orders_joined из другой mart, если понадобится. Финальный скомпилированный SQL будет одинаковый.

TIP

Ephemeral отлично для “DRY” — когда логика повторяется в 1-2 моделях, и хочется не дублировать SQL. Но если повторяется в 3+ — лучше view, чтобы не повторять вычисления в warehouse каждый раз.

DuckDB-специфика для ephemeral

Ephemeral на DuckDB работает так же, как на других warehouse — это чисто компиляционный концепт dbt, warehouse его не знает. Но есть несколько нюансов:

  1. DuckDB CTE-планировщик агрессивный. Простой CTE inlined эффективно, на маленьких объёмах разница ephemeral vs view не заметна.
  2. На больших данных не злоупотребляй. DuckDB query planner — vectorized, но не магия. Длинный WITH с GROUP BY на каждом уровне всё равно медленнее, чем готовая table.
  3. target/compiled показывает всё. Чтобы посмотреть финальный SQL после ephemeral inlining — открой target/compiled/<project>/models/.../mart_revenue.sql. Там все CTE раскрыты.

Команды для отладки ephemeral

# Компилируем downstream и смотрим, как ephemeral inlined
dbt compile --select mart_revenue
cat target/compiled/jaffle_shop/models/marts/mart_revenue.sql

В файле увидишь WITH int_orders_joined AS (…) — это и есть встроенная ephemeral-модель.

# Попытка запустить ephemeral напрямую
dbt run --select int_orders_joined

Output:

14:23:01  Concurrency: 4 threads
Done. PASS=0 WARN=0 ERROR=0 SKIP=0 TOTAL=0

Ноль операций — ephemeral не материализуется, и dbt run “ничего не делает” в warehouse, только парсит модель.

Попробуй сам

В своём проекте создай models/intermediate/int_test_ephemeral.sql:

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

SELECT
    customer_id,
    UPPER(email) AS email_upper
FROM {{ ref('stg_customers') }}

И mart, который её использует:

-- models/marts/test_mart.sql
{{ config(materialized='table') }}

SELECT *
FROM {{ ref('int_test_ephemeral') }}
WHERE email_upper LIKE '%@JAFFLE.COM'

Запусти:

dbt run --select test_mart

Только test_mart создастся в warehouse (как BASE TABLE). int_test_ephemeral — не создаётся.

Открой target/compiled/jaffle_shop/models/marts/test_mart.sql — увидишь WITH int_test_ephemeral AS (…) и финальный SELECT. Это и есть inline-материализация.

Что мы поняли

Ephemeral — материализация-CTE, которая не создаёт ничего в warehouse, а встраивается в downstream-модели как WITH-блок. Полезна для приватных промежуточных шагов с одним downstream и для выноса логики из subquery в отдельный файл. Главные ловушки: deep nesting (3+ уровня делают SQL монструозным), нерабочие тесты, повторные вычисления при использовании в нескольких downstream. Используй с осторожностью — view или table обычно безопаснее.

В следующем уроке разберём Golden Rule — эмпирическое правило, как выбрать материализацию для каждой модели.

CTE как SQL-конструкция
Проверка знанийKnowledge check
Ты хочешь повесить not_null тест на колонку order_id в модели int_orders_filtered, которая материализована как ephemeral. После dbt test видишь, что тест не запускается. Почему и что делать?
ОтветAnswer
Ephemeral модели не создаются в warehouse, поэтому SELECT FROM <model> в тесте не работает — таблицы нет. dbt пропускает тесты на ephemeral молча (в зависимости от версии — warning). Решение: либо перевести модель в view (тесты заработают, минимальный overhead, данные всегда свежие), либо в table (если модель дорогая в пересчёте). Любая testable модель не должна быть ephemeral.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Что происходит в warehouse при materialized='ephemeral'?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4