Learning Platform
Глоссарий Troubleshooting
Урок 14.02 · 22 мин
Начальный
dbtModelsrefMaterialization

Модели и 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:

  1. Прочитает файл, развернёт {{ source('raw', 'orders') }} в полное имя raw.orders.
  2. Создаст из имени файла имя модели: stg_orders.
  3. Обернёт SQL в CREATE OR REPLACE VIEW stg_orders AS ... (по умолчанию модели материализуются как view).
  4. Отправит финальный 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.

TIP

Всегда оборачивай сырые таблицы в 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-схемы.

Граф моделей: слои

В дбт-сообществе принято разделять модели на слои. Это не требование, а соглашение, но его придерживается большинство команд.

Типичный граф dbt-моделей
raw.orders
raw.customers
stg_orders
stg_customers
int_orders_enriched
mart_orders_daily
mart_customers_summary
  • 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 %}

При первом запуске создаёт таблицу. При следующих — только догружает новые строки. Огромная экономия времени на больших таблицах.

Когда использовать: факт-таблицы с миллиардами строк, кликстрим, события.

WARNING

Инкрементальные модели — самые сложные в 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-i: staging/intermediate/marts — production-grade структура проекта dbt snapshots: автоматический SCD Type 2 для track изменений
NOTE

Это лишь стартовая база. Полный курс по 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 — увидишь свои модели визуально.

Проверка знанийKnowledge check
Зачем использовать ref('stg_orders') вместо прямого имени analytics.stg_orders в SQL-модели?
ОтветAnswer
Три ключевые причины. Во-первых, ref() строит граф зависимостей DAG: dbt узнаёт, что одна модель зависит от другой, и запускает их в правильном порядке. Во-вторых, ref() обеспечивает изоляцию окружений: в dev-окружении он подставит имя таблицы из dev-схемы, в prod — из prod-схемы, без правок SQL. В-третьих, ref() даёт безопасное переименование: если меняется схема DWH или имя модели, нужно обновить одно место, а dbt подставит правильное имя везде. Прямое имя таблицы лишает всех этих преимуществ — это анти-паттерн в dbt.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 6. Что делает ref('stg_orders') в dbt-модели при компиляции?

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

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

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

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