В этом модуле мы разбираемся с материализациями — главной концепцией, которая отделяет dbt от обычного SQL. Материализация — это способ, которым модель попадает в warehouse: как view, как полноценная таблица, как ephemeral CTE или инкрементально. Разные материализации — разные trade-off между скоростью и стоимостью.
Начинаем с самой простой: view. Это материализация по умолчанию. Если ты создал модель и не написал {'{{'} config(...) {'}}'}, она будет view.
Что такое view в warehouse
View — это сохранённый SQL-запрос с именем. В warehouse физически нет таблицы с данными, есть только метаданные: “когда кто-то делает SELECT FROM stg_customers, выполни вот этот запрос”. Сами данные не дублируются.
dbt берёт SQL из файла модели, оборачивает его в CREATE OR REPLACE VIEW и отправляет в warehouse. Сами данные не копируются — view хранит только query plan.
Когда 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.
Эмпирическое правило: 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 делает:
- Парсит модель, компилирует Jinja -> получает чистый SQL.
- Берёт materialization=
view-> выбирает шаблон CREATE VIEW. - Применяет шаблон к компилированному SQL -> получает финальный DDL/DML.
- Отправляет финальный 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 уже есть — заменится. Если нет — создастся.
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:
- Раскрывает mart_orders_daily -> его SQL содержит SELECT FROM int_order_metrics.
- Раскрывает int_order_metrics -> его SQL содержит SELECT FROM stg_orders.
- Раскрывает stg_orders -> его SQL содержит SELECT FROM raw.orders.
- Получается один большой запрос с CTE на каждой стадии. Warehouse его планирует и выполняет.
Каждый SELECT с конца чейна раскрывает все view вверх по DAG в один большой запрос. На 3-5 уровнях это уже заметная latency для warehouse-планировщика.
На 1-2 уровнях view это нормально. На 5+ — warehouse начинает тратить время на парсинг и планирование. Решение: материализовать промежуточные слои как table.
DuckDB-специфика для view
В DuckDB view работает почти как в Postgres. Несколько нюансов:
- CREATE OR REPLACE VIEW поддерживается — стандартный sytanx, dbt-duckdb работает прямо.
- View хранится в catalog файле, физически не занимает место на диске.
- SELECT FROM view быстрый — DuckDB inlining агрессивный, query planner раскрывает view в один план без overhead. На небольших данных это значит, что разница view vs table может быть незаметна.
- Нет 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