Модели и ref(): как строить граф трансформаций
В dbt всё крутится вокруг моделей. Модель — это файл .sql, который содержит один SELECT. Файл попадает в папку models/, и dbt автоматически узнаёт о нём. Никаких регистраций, никаких манифестов — просто положи файл в нужное место.
Но настоящая магия dbt начинается, когда модели ссылаются друг на друга через функцию ref(). Так строится граф зависимостей (DAG), и dbt сам понимает, в каком порядке всё запускать.
В этом уроке разберём, как писать модели, как их связывать и какие бывают типы материализации.
Как выглядит модель dbt
Модель — это .sql файл с одним SELECT. Без CREATE TABLE, без INSERT, без MERGE. Только запрос.
models/staging/stg_orders.sql:
SELECT
order_id,
customer_id,
amount,
status,
created_at
FROM {{ source('raw', 'orders') }}
WHERE created_at >= '2020-01-01'
Когда ты запускаешь dbt run, dbt:
- Прочитает файл, развернёт
{{ source('raw', 'orders') }}в полное имяraw.orders. - Создаст из имени файла имя модели:
stg_orders. - Обернёт SQL в
CREATE OR REPLACE VIEW stg_orders AS ...(по умолчанию модели материализуются какview). - Отправит финальный SQL в DWH.
После этого в DWH появится view под названием stg_orders — и можно его использовать в других моделях.
Source: ссылка на сырые таблицы
Сырые данные, загруженные через Fivetran или Airbyte, лежат в DWH без участия dbt. Чтобы dbt о них знал, их объявляют как sources в YAML-файле:
models/staging/_sources.yml:
version: 2
sources:
- name: raw
schema: raw_data
tables:
- name: orders
description: "Сырые заказы из биллинга, грузятся Fivetran"
columns:
- name: order_id
description: "Уникальный ID заказа"
- name: customers
description: "Сырые клиенты из CRM"
В SQL потом обращаешься так: {{ source('raw', 'orders') }}. dbt развернёт это в raw_data.orders.
Всегда оборачивай сырые таблицы в source(). Тогда при переименовании или переезде схемы исходных данных нужно поменять одно место — YAML-файл, а не десятки SQL-моделей.
ref(): главная функция dbt
Когда одна модель ссылается на другую, никогда не пиши имя таблицы напрямую. Используй ref():
models/intermediate/int_orders_enriched.sql:
SELECT
o.order_id,
o.amount,
c.country,
c.signup_date
FROM {{ ref('stg_orders') }} o
LEFT JOIN {{ ref('stg_customers') }} c
ON o.customer_id = c.customer_id
WHERE o.status = 'paid'
{{ ref('stg_orders') }} — это шаблон Jinja. При компиляции он превратится в полное имя таблицы: analytics.stg_orders (или какое там у вас имя схемы).
Что даёт ref():
- Граф зависимостей. dbt видит, что
int_orders_enrichedзависит отstg_ordersиstg_customers. Поэтому он запустит сначала их, потом —int_orders_enriched. - Lineage. В документации появится граф связей моделей.
- Безопасное переименование. Изменишь схему DWH — dbt сам подставит новые имена.
- Изоляция окружений. В dev-окружении
ref()подставит таблицы из dev-схемы, в prod — из prod-схемы.
Граф моделей: слои
В дбт-сообществе принято разделять модели на слои. Это не требование, а соглашение, но его придерживается большинство команд.
- Staging (
stg_) — 1:1 с источником, лёгкая очистка. Один файл на одну таблицу из источника. Цель — стандартизировать имена и типы. - Intermediate (
int_) — промежуточные модели с бизнес-логикой: джойны, расчёты, агрегаты. Не для прямого потребления BI. - Marts (
mart_илиfct_/dim_) — финальные витрины для аналитиков и дашбордов.
Эта структура — рекомендация dbt Labs (“dbt project structure”). Следуй ей с первого проекта.
Четыре типа материализации
Материализация — это как dbt физически создаёт модель в DWH. Указывается через config.
1. view (по умолчанию)
{{ config(materialized='view') }}
SELECT ...
dbt создаст CREATE OR REPLACE VIEW. Хранения нет — запрос выполняется при каждом обращении.
Когда использовать: маленькие staging-модели, простые трансформации, не нужны быстрые чтения.
2. table
{{ config(materialized='table') }}
SELECT ...
dbt создаст полноценную таблицу через CREATE OR REPLACE TABLE AS SELECT. Каждый dbt run пересоздаёт её с нуля.
Когда использовать: marts с миллионами строк, нужны быстрые чтения, данные относительно небольшие (миллионы, не миллиарды).
3. incremental
{{ config(materialized='incremental', unique_key='order_id') }}
SELECT *
FROM {{ ref('stg_orders') }}
{% if is_incremental() %}
WHERE created_at > (SELECT max(created_at) FROM {{ this }})
{% endif %}
При первом запуске создаёт таблицу. При следующих — только догружает новые строки. Огромная экономия времени на больших таблицах.
Когда использовать: факт-таблицы с миллиардами строк, кликстрим, события.
Инкрементальные модели — самые сложные в dbt. Нужно правильно выбрать unique_key и условие фильтрации, иначе получишь дубли или пропуски. Делай инкрементальной только тогда, когда полная перестройка реально дорогая.
4. ephemeral
{{ config(materialized='ephemeral') }}
SELECT ...
В DWH ничего не создаётся. Когда другая модель ссылается на ephemeral, dbt подставляет её SQL как CTE.
Когда использовать: маленькие промежуточные расчёты, которые не нужно материализовать отдельно. Редко.
Конфиг на уровне папки
Часто хочется задать материализацию для всех моделей в папке. Это делается в dbt_project.yml:
models:
my_project:
staging:
+materialized: view # все stg_ модели — view
intermediate:
+materialized: ephemeral # int_ модели — CTE
marts:
+materialized: table # mart_ модели — полные таблицы
Конкретная модель может переопределить это в своём config(). Иерархия: file > folder > project.
Запуск и выбор моделей
dbt run — запускает все модели проекта в правильном порядке.
dbt run --select stg_orders — только одну модель.
dbt run --select +mart_orders_daily — модель и все её upstream зависимости (родителей).
dbt run --select stg_orders+ — модель и все её downstream зависимости (детей).
dbt run --select tag:nightly — модели с тегом nightly.
Эти селекторы — мощный инструмент для CI/CD и инкрементальных деплоев.
Пример полного пайплайна
Соберём вместе пример: stg_orders -> int_orders_enriched -> mart_orders_daily.
models/staging/stg_orders.sql:
SELECT
order_id,
customer_id,
amount,
status,
created_at AS order_at
FROM {{ source('raw', 'orders') }}
models/intermediate/int_orders_enriched.sql:
{{ config(materialized='ephemeral') }}
SELECT
o.order_id,
o.customer_id,
o.amount,
o.order_at,
c.country
FROM {{ ref('stg_orders') }} o
LEFT JOIN {{ ref('stg_customers') }} c USING (customer_id)
WHERE o.status = 'paid'
models/marts/mart_orders_daily.sql:
{{ config(materialized='table') }}
SELECT
date_trunc('day', order_at) AS day,
country,
count(*) AS orders_cnt,
sum(amount) AS revenue
FROM {{ ref('int_orders_enriched') }}
GROUP BY 1, 2
Запускаешь dbt run — dbt сам определит порядок: stg_orders и stg_customers идут параллельно, потом int_orders_enriched, потом mart_orders_daily.
Что ещё умеет dbt
Этот урок дал базу — модели, ref(), материализации. Но в реальном dbt-проекте есть ещё несколько важных вещей, которые junior DE должен хотя бы знать на уровне “что это такое и когда применять”. Полностью разбирать их в этом обзорном модуле мы не будем — для каждой темы нужен отдельный глубокий разбор.
Schema tests (декларативные проверки качества). В YAML рядом с описанием модели объявляешь: unique для первичного ключа, not_null для обязательных колонок, accepted_values: ['paid', 'pending', 'cancelled'] для статусов, relationships для проверки FK. dbt сам сгенерирует SQL-запросы, которые проверят данные. Запускаешь dbt test — если какой-то тест падает, pipeline валится и команда узнаёт о проблеме раньше аналитика. Это базовый инструмент качества данных, его настраивают в первый же день dbt-проекта.
Документация (dbt docs). В тех же YAML можно описать каждую модель и каждую колонку. Команда dbt docs generate собирает все описания, скомпилированные SQL и граф lineage в статический сайт. Аналитик открывает в браузере, видит описание mart_revenue_daily, какие upstream-модели его формируют, как считаются метрики. Хостится в S3 или GitHub Pages. Решает класс проблем “как считается эта метрика”.
Jinja и macros (переиспользуемая логика). SQL в dbt — это не чистый SQL, а Jinja-шаблон. Можно писать {% if %}, {% for %}, объявлять переменные. Когда одинаковая логика повторяется в десятках моделей (например, конвертация центов в доллары), её выносят в макрос cents_to_dollars(column) в папке macros/ и вызывают везде. DRY-принцип в SQL.
Packages (готовые библиотеки для dbt). dbt Hub хостит сотни open-source пакетов. Самые популярные: dbt_utils (универсальные хелперы — pivot, surrogate_key, date_spine), dbt_expectations (расширенные тесты в стиле Great Expectations), коннекторы к Salesforce, Stripe, Google Analytics. Подключаются через packages.yml — это аналог npm/pip для dbt-проектов.
Snapshots для SCD2. Если в источнике хранится только текущее состояние (например, customers.plan), а аналитику нужна история — как менялся план у клиента — dbt умеет фиксировать SCD2. Объявляешь snapshot в snapshots/, запускаешь dbt snapshot регулярно (раз в час). dbt сравнивает текущее состояние с прошлым и пишет историю с dbt_valid_from/dbt_valid_to. Через полгода у тебя полная история измерения.
Это лишь стартовая база. Полный курс по dbt с тестами, документацией, Jinja, macros, packages, snapshots, инкрементальными моделями, CI/CD и продакшен-паттернами — в нашем курсе dbt-i на платформе.
Попробуй сам
Возьми любые две связанные таблицы в твоей БД (например, orders и customers). Напиши для них staging-модели через source(). Потом создай mart-модель, которая джойнит их через ref(). Поэкспериментируй с материализациями: попробуй view, table, ephemeral. Запусти dbt run, посмотри в DWH, что создалось. Запусти dbt docs generate, открой dbt docs serve, и посмотри граф lineage — увидишь свои модели визуально.