Learning Platform
Глоссарий Troubleshooting
Урок 07.01 · 16 мин
Начальный
materializationviewdefaultCREATE VIEW

В этом модуле мы разбираемся с материализациями — главной концепцией, которая отделяет dbt от обычного SQL. Материализация — это способ, которым модель попадает в warehouse: как view, как полноценная таблица, как ephemeral CTE или инкрементально. Разные материализации — разные trade-off между скоростью и стоимостью.

Начинаем с самой простой: view. Это материализация по умолчанию. Если ты создал модель и не написал {'{{'} config(...) {'}}'}, она будет view.

Что такое view в warehouse

View — это сохранённый SQL-запрос с именем. В warehouse физически нет таблицы с данными, есть только метаданные: “когда кто-то делает SELECT FROM stg_customers, выполни вот этот запрос”. Сами данные не дублируются.

Что делает dbt при материализации модели как view

dbt берёт SQL из файла модели, оборачивает его в CREATE OR REPLACE VIEW и отправляет в warehouse. Сами данные не копируются — view хранит только query plan.

models/stg_customers.sqlSELECT id, email FROM source(...)
dbt runкомпилирует Jinja
Отправляется в warehouseCREATE OR REPLACE VIEW analytics.stg_customers AS (SELECT ...)
Результат в warehouseтолько метаданные, данные не копируются

Когда downstream-модель или дашборд делает SELECT * FROM stg_customers, warehouse каждый раз выполняет полный исходный запрос (с join, фильтрами, кастом). Это значит: данные всегда свежие, занимают 0 байт лишнего места — но запрос медленнее, чем чтение из готовой таблицы.

Когда view — хороший выбор

View идеально подходит, когда:

1. Запрос дешёвый. Простой SELECT с парой переименований/cast — повторное выполнение почти бесплатно. На staging-моделях обычно это так: id -> customer_id, парочка CAST(... AS DATE), фильтр на soft-deleted. Не имеет смысла материализовать в table — пересчёт за миллисекунды.

2. Данные часто меняются. Если raw обновляется каждые 15 минут, а ты материализуешь в table — нужно делать dbt run после каждой загрузки. View всегда читает свежее.

3. Низкий traffic на downstream. Если у view 2-3 downstream-модели, и они запускаются раз в день — повторное выполнение не вызовет нагрузку. Если 50 dashboards читают её каждые 5 минут — придётся материализовать в table.

4. Низкий cost storage важнее performance. На больших данных таблицы стоят денег за storage. View — бесплатно. На junior-проекте редко актуально, но для cloud warehouse’ов имеет значение.

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

1. Тяжёлые JOIN и aggregations. Если модель агрегирует 10M строк с групировкой и оконными функциями, и downstream обращается к ней 10 раз — warehouse выполнит этот тяжёлый запрос 10 раз. Лучше один раз посчитать в table, дальше читать как готовое.

2. Чейн view -> view -> view -> view. Каждый downstream-вызов раскрывает всю цепочку запросов. На третьем уровне это уже несколько секунд parser-у warehouse, на пятом — заметная latency. Глубокие чейны view — антипаттерн.

3. Когда нужны индексы/partitions. View не имеет индексов, потому что нет данных. Если ты планируешь использовать модель как fact table для крупных query — материализуй в table и добавь partitioning.

TIP

Эмпирическое правило: staging — view (cheap transformations), intermediate — view или table (зависит от веса), marts — table (часто читается, медленно считается). View по умолчанию — это удобно, потому что 80% staging-моделей не требуют ничего больше.

Explicit config: когда нужно

Если ты хочешь явно сказать “это view” — в SQL-файле модели сверху:

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

SELECT
    id        AS customer_id,
    email
FROM {{ source('jaffle_shop', 'raw_customers') }}

Если ничего не указано — поведение то же. Указывать имеет смысл, когда:

  • проект задаёт другой default через dbt_project.yml;
  • хочешь сделать явный комментарий “это view, не нужно table”;
  • планируешь добавить другие параметры в config.

Можно задать default для целой папки в dbt_project.yml:

models:
  jaffle_shop:
    staging:
      +materialized: view
    marts:
      +materialized: table

Это значит: всё в models/staging/ — view, всё в models/marts/ — table. На уровне конкретной модели можно переопределить.

Что происходит при dbt run для view

Когда ты запускаешь dbt run --select stg_customers, dbt делает:

  1. Парсит модель, компилирует Jinja -> получает чистый SQL.
  2. Берёт materialization=view -> выбирает шаблон CREATE VIEW.
  3. Применяет шаблон к компилированному SQL -> получает финальный DDL/DML.
  4. Отправляет финальный SQL в warehouse через адаптер.

Финальный SQL (в target/run/...) выглядит примерно так:

CREATE OR REPLACE VIEW jaffle_shop.main.stg_customers AS (
    WITH source AS (
        SELECT * FROM jaffle_shop.main.raw_customers
    ),
    renamed AS (
        SELECT
            id        AS customer_id,
            first_name,
            last_name
        FROM source
    )
    SELECT * FROM renamed
);

CREATE OR REPLACE — это потому что dbt идемпотентен: один и тот же dbt run можно запускать многократно, результат тот же. Если view уже есть — заменится. Если нет — создастся.

WARNING

CREATE OR REPLACE VIEW в DuckDB и Postgres работает, но в Snowflake/BigQuery есть нюансы: если схема изменилась, на каких-то warehouse может потребоваться DROP VIEW. dbt разруливает это в шаблонах materialization. Для junior’а на DuckDB можно про это не помнить.

Производительность view: чейны и query plans

Главная ловушка view-материализации — раскрытие при каждом обращении. Возьмём упрощённый чейн:

raw.orders -> stg_orders (view) -> int_order_metrics (view) -> mart_orders_daily (view) -> dashboard

Когда dashboard делает SELECT FROM mart_orders_daily, warehouse:

  1. Раскрывает mart_orders_daily -> его SQL содержит SELECT FROM int_order_metrics.
  2. Раскрывает int_order_metrics -> его SQL содержит SELECT FROM stg_orders.
  3. Раскрывает stg_orders -> его SQL содержит SELECT FROM raw.orders.
  4. Получается один большой запрос с CTE на каждой стадии. Warehouse его планирует и выполняет.
Раскрытие чейна view при SELECT

Каждый SELECT с конца чейна раскрывает все view вверх по DAG в один большой запрос. На 3-5 уровнях это уже заметная latency для warehouse-планировщика.

dashboard querySELECT * FROM mart_orders_daily LIMIT 10
unfold view 1Warehouse видит SELECT FROM view, начинает раскрывать SQL. На SQL-уровне это превращается в WITH CTE-ами на каждый уровень.
unfold view 2int_order_metrics — тоже view. SQL раскрывается. Глубже идём.
unfold view 3stg_orders — view. SQL раскрывается до source.
Final queryодин большой WITH с 3 CTE + raw scan

На 1-2 уровнях view это нормально. На 5+ — warehouse начинает тратить время на парсинг и планирование. Решение: материализовать промежуточные слои как table.

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

В DuckDB view работает почти как в Postgres. Несколько нюансов:

  1. CREATE OR REPLACE VIEW поддерживается — стандартный sytanx, dbt-duckdb работает прямо.
  2. View хранится в catalog файле, физически не занимает место на диске.
  3. SELECT FROM view быстрый — DuckDB inlining агрессивный, query planner раскрывает view в один план без overhead. На небольших данных это значит, что разница view vs table может быть незаметна.
  4. Нет materialized view — DuckDB не поддерживает CREATE MATERIALIZED VIEW. На других warehouse это есть, но в dbt-duckdb materialization ‘materialized_view’ недоступна.

Команды для отладки view-материализации

Запустить и проверить:

dbt run --select stg_customers

Output:

14:23:01  Concurrency: 4 threads
14:23:01  1 of 1 START sql view model main.stg_customers ........... [RUN]
14:23:01  1 of 1 OK created sql view model main.stg_customers ....... [OK in 0.12s]

Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1

Видишь “sql view model” — это подтверждение, что dbt использовал view-материализацию. Время создания view минимальное, потому что данных нет.

Проверить в DuckDB CLI:

duckdb jaffle_shop.duckdb
SELECT table_name, table_type FROM information_schema.tables WHERE table_name = 'stg_customers';

Получишь:

table_name      table_type
stg_customers   VIEW

VIEW в table_type — подтверждение материализации.

Попробуй сам

В своём Jaffle Shop проекте создай models/staging/stg_test_view.sql:

SELECT 1 AS dummy_id, 'view-test' AS dummy_name

Без config — будет view по умолчанию. Запусти:

dbt run --select stg_test_view

Проверь:

duckdb jaffle_shop.duckdb -c "SELECT table_name, table_type FROM information_schema.tables WHERE table_name='stg_test_view';"

Затем добавь {'{{'} config(materialized='view') {'}}'} сверху файла. Запусти dbt run снова — поведение то же, view пересоздалась. Это explicit-вариант того же default.

Что мы поняли

View — материализация по умолчанию в dbt. Физически в warehouse — сохранённый SQL-запрос без данных. Хороший выбор для дешёвых staging-моделей, которые читаются нечасто и где данные должны быть всегда свежими. Плохой выбор для тяжёлых aggregations с высоким downstream-traffic’ом и глубоких чейнов view -> view -> view. На DuckDB view работает быстро, потому что планировщик агрессивно inline-ит, но на больших cloud warehouse’ах разница view vs table критична.

В следующем уроке разберём table — когда от view имеет смысл перейти к полноценной таблице.

Views и materialized views в SQL
Проверка знанийKnowledge check
У тебя модель monthly_revenue, посчитанная как view, агрегирует 5M строк orders с GROUP BY и оконными функциями. На неё ссылаются 12 dashboards, каждый раз пересчитывая 5M строк. Что улучшить?
ОтветAnswer
Перевести monthly_revenue в table materialization: {'{{'} config(materialized='table') {'}}'}. Один раз посчитается в dbt run, дальше дашборды читают готовые данные. Если запрос дорогой, а downstream-частота высокая — table почти всегда лучше view. Свежесть достигается за счёт регулярного dbt run (например ночного). Если данные обновляются часто и нужны почти-real-time — рассмотри incremental materialization, которая считает только дельту.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Какая материализация используется по умолчанию, если в модели не указан config()?

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

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

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

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