SQLLab
Все статьи

dbt и SQL: трансформация данных в современном стеке

Что такое dbt, как он использует SQL для трансформации данных: модели, ref(), тесты, документация, lineage. Введение для аналитиков и дата-инженеров.

20 марта 2026 г.·4 мин чтения·

dbt (data build tool) — инструмент, который превратил SQL из языка запросов в язык инженерии данных. С dbt аналитики пишут SELECT-запросы, а инфраструктура сама строит пайплайны.

Что такое dbt

dbt берёт ваши SQL SELECT-запросы и запускает их в нужном порядке, создавая таблицы или views в базе данных. Никакого Python, никакого ETL-кода — только SQL.

-- Обычный SQL-запрос (dbt-модель):
-- models/marts/user_stats.sql
SELECT
    u.id,
    u.email,
    COUNT(o.id) AS total_orders,
    SUM(o.amount) AS total_spent,
    MAX(o.created_at) AS last_order_at
FROM {{ ref('stg_users') }} u
LEFT JOIN {{ ref('stg_orders') }} o ON o.user_id = u.id
GROUP BY u.id, u.email

{{ ref('stg_users') }} — ссылка на другую dbt-модель. dbt строит граф зависимостей и запускает модели в правильном порядке.


Слои трансформаций

Стандартная архитектура dbt:

Raw (исходные данные)
    ↓
Staging (stg_*) — очистка и переименование
    ↓
Intermediate (int_*) — бизнес-логика
    ↓
Marts (fct_*, dim_*) — финальные таблицы для аналитики
-- models/staging/stg_orders.sql
SELECT
    id AS order_id,
    user_id,
    amount,
    LOWER(status) AS status,
    created_at::timestamptz AS created_at
FROM {{ source('raw', 'orders') }}
WHERE amount > 0  -- базовая очистка
-- models/marts/fct_orders.sql
SELECT
    o.order_id,
    o.user_id,
    u.email,
    u.segment,
    o.amount,
    o.status,
    o.created_at,
    DATE_TRUNC('month', o.created_at) AS order_month
FROM {{ ref('stg_orders') }} o
JOIN {{ ref('stg_users') }} u ON u.user_id = o.user_id

Материализация моделей

# dbt_project.yml
models:
  my_project:
    staging:
      +materialized: view       # лёгкие staging — вью
    marts:
      +materialized: table      # финальные таблицы
    intermediate:
      +materialized: ephemeral  # CTE, не создаёт объект в БД

В самой модели:

-- models/marts/daily_metrics.sql
{{ config(materialized='table', post_hook='ANALYZE {{ this }}') }}

SELECT
    DATE_TRUNC('day', created_at) AS day,
    COUNT(*) AS orders,
    SUM(amount) AS revenue
FROM {{ ref('stg_orders') }}
GROUP BY 1

Тесты качества данных

dbt имеет встроенные тесты:

# models/staging/schema.yml
models:
  - name: stg_orders
    columns:
      - name: order_id
        tests:
          - unique          # нет дубликатов
          - not_null        # нет NULL
      - name: status
        tests:
          - accepted_values:
              values: ['pending', 'completed', 'refunded', 'cancelled']
      - name: user_id
        tests:
          - not_null
          - relationships:
              to: ref('stg_users')
              field: user_id
dbt test  # запустить все тесты
dbt test --select stg_orders  # только для одной модели

Пользовательские тесты

-- tests/test_no_future_orders.sql
-- Тест проходит если запрос возвращает 0 строк
SELECT * FROM {{ ref('stg_orders') }}
WHERE created_at > NOW()

Incremental-модели

-- models/marts/events_incremental.sql
{{ config(materialized='incremental', unique_key='event_id') }}

SELECT
    id AS event_id,
    user_id,
    event_type,
    created_at
FROM {{ source('raw', 'events') }}

{% if is_incremental() %}
-- При инкрементальном запуске — только новые строки
WHERE created_at > (SELECT MAX(created_at) FROM {{ this }})
{% endif %}

Первый запуск — полная загрузка. Последующие — только новые данные.


Seeds: загрузка CSV

# seeds/country_codes.csv
code,country_name,region
RU,Россия,Europe
US,United States,Americas
DE,Germany,Europe
dbt seed  # загружает CSV как таблицу в БД
-- Использование в модели
SELECT o.*, c.country_name
FROM {{ ref('fct_orders') }} o
JOIN {{ ref('country_codes') }} c ON c.code = o.country_code

Macros: переиспользуемый SQL

-- macros/safe_divide.sql
{% macro safe_divide(numerator, denominator) %}
    CASE WHEN {{ denominator }} = 0 THEN NULL
         ELSE {{ numerator }}::numeric / {{ denominator }}
    END
{% endmacro %}

-- Использование в модели:
SELECT
    category,
    conversions,
    {{ safe_divide('conversions', 'sessions') }} AS cvr
FROM {{ ref('fct_sessions') }}

Документация

# models/marts/schema.yml
models:
  - name: fct_orders
    description: "Факт-таблица заказов. Обновляется ежедневно в 3:00 UTC."
    columns:
      - name: order_id
        description: "Уникальный ID заказа из системы оплаты"
      - name: amount
        description: "Сумма заказа в рублях без НДС"
dbt docs generate  # сгенерировать документацию
dbt docs serve     # открыть в браузере с lineage graph

Типичный стек с dbt

PostgreSQL / Snowflake / BigQuery  — хранение данных
    ↑ сырые данные
Airbyte / Fivetran / Stitch        — репликация из источников
    ↓ трансформации
dbt                                — трансформации SQL
    ↓ готовые таблицы
Metabase / Superset / Tableau      — дашборды

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

✅ Много SQL-трансформаций, которые зависят друг от друга

✅ Команда аналитиков пишет SQL (не Python)

✅ Нужно версионирование, тесты и документация SQL-кода

✅ Хранилище данных: Snowflake, BigQuery, Redshift, PostgreSQL

❌ Простые запросы без зависимостей

❌ Источники данных не в SQL-совместимой БД


Итог

dbt — это Git + тесты + документация + зависимости для SQL. Если вы пишете сложные SQL-трансформации и хотите порядка в коде, dbt — стандарт индустрии в 2026.

# Запуск
dbt run          # выполнить все модели
dbt test         # запустить все тесты
dbt build        # run + test
dbt run --select my_model+  # модель и все её зависимые

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

Открыть тренажёр →