Learning Platform
Глоссарий Troubleshooting
Урок 05.02 · 20 мин
Начальный
refDAGlineagejinja

ref(): соединяем модели в граф

ref() — это главная функция dbt. Без неё dbt — это просто набор SQL-файлов с CLI-обёрткой. С ref() — это полноценный transformation framework.

В этом уроке мы разберём, что именно делает ref(), почему это так важно, и как его использовать на практике.


Что делает ref()

Когда вы пишете внутри модели:

SELECT *
FROM {{ ref('stg_orders') }}
WHERE amount > 100

dbt при компиляции делает две вещи одновременно:

  1. Подставляет полное имя таблицы на место {{ ref('stg_orders') }}:
    SELECT * FROM "dev"."main"."stg_orders" WHERE amount > 100
  2. Добавляет ребро в DAG зависимостей: «текущая модель зависит от stg_orders».

Эти две вещи — подстановка имени + регистрация зависимости — и делают ref() особенным.

CTE (WITH ... AS) — структурный предшественник ref() в чистом SQL

Почему не просто хардкод имени

«А почему бы просто не написать SELECT * FROM dev.main.stg_orders?» — частый вопрос джунов. Несколько причин:

1. Environment portability

Хардкод имени работает в одном environment. Но dev.main.stg_orders в dev — это prod.main.stg_orders в prod. Без ref() модель пришлось бы переписывать для каждого окружения.

С ref() dbt сам подставляет правильное имя в зависимости от target:

  • target: dev -> "dev"."main"."stg_orders"
  • target: prod -> "analytics"."public"."stg_orders"

Один и тот же SQL работает везде.

2. Автоматическое построение DAG

Без ref() зависимости между моделями приходится описывать вручную (в Airflow это set_upstream). С ref() dbt строит DAG из самого SQL: парсит {{ ref('X') }} и добавляет ребро. На 500 моделях это переход от ручного управления зависимостями к автоматическому.

3. Топологический порядок выполнения

dbt run/build выполняет модели в правильном порядке (parents -> children). Без ref() dbt не знает, что от чего зависит, и пришлось бы ручкой указывать порядок.

4. dbt run —select X+ работает только с ref()

Команда dbt run --select stg_orders+ запускает stg_orders и всё, что от неё зависит. Без ref() dbt не знает, что зависит — этот функционал не работает.

5. lineage UI и docs

dbt docs serve рисует lineage-диаграммы. Они генерируются из ref()-зависимостей. Без ref() — пустой граф.


Минимальный пример: parent + child

Создадим два связанных модели.

models/staging/stg_orders.sql:

SELECT
    100 AS order_id, 1 AS customer_id, 50.00 AS amount, '2026-01-01'::date AS order_date
UNION ALL SELECT 101, 2, 75.50, '2026-01-15'::date
UNION ALL SELECT 102, 1, 120.00, '2026-02-01'::date
UNION ALL SELECT 103, 3, 30.00, '2026-02-05'::date

models/marts/orders_large.sql:

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

SELECT
    order_id,
    customer_id,
    amount,
    order_date
FROM {{ ref('stg_orders') }}
WHERE amount > 50

Запустим:

dbt run --select orders_large+

Стоп, опечатка: нам нужно build с верхней модели:

dbt run --select +orders_large

Префикс + слева означает «эта модель и все upstream». Вывод:

06:55:11  Found 2 models, 408 macros
06:55:11
06:55:11  Concurrency: 4 threads (target='dev')
06:55:11
06:55:11  1 of 2 START sql view model main.stg_orders ......... [RUN]
06:55:11  1 of 2 OK created sql view model main.stg_orders .... [OK in 0.03s]
06:55:11  2 of 2 START sql table model main.orders_large ...... [RUN]
06:55:11  2 of 2 OK created sql table model main.orders_large . [OK in 0.04s]

Заметили? Сначала stg_orders (view), потом orders_large (table). dbt сам определил порядок из {{ ref('stg_orders') }} в orders_large.

Проверяем в DuckDB:

SELECT * FROM orders_large;
-- ┌──────────┬─────────────┬────────┬─────────────┐
-- │ order_id │ customer_id │ amount │ order_date  │
-- ├──────────┼─────────────┼────────┼─────────────┤
-- │      101 │           2 │   75.50│ 2026-01-15  │
-- │      102 │           1 │  120.00│ 2026-02-01  │
-- └──────────┴─────────────┴────────┴─────────────┘

Две строки (amount > 50). Отлично.


Что в compiled SQL

Откроем compiled:

cat target/compiled/learning_models/models/marts/orders_large.sql
SELECT
    order_id,
    customer_id,
    amount,
    order_date
FROM "dev"."main"."stg_orders"
WHERE amount > 50

{{ ref('stg_orders') }} заменилось на "dev"."main"."stg_orders". Если бы target был prod с другой базой/схемой — здесь стояло бы её имя.

run-версия добавляет DDL:

create or replace table "dev"."main"."orders_large"
  
  as (
    SELECT
        order_id,
        customer_id,
        amount,
        order_date
    FROM "dev"."main"."stg_orders"
    WHERE amount > 50
  );

Это то, что DuckDB реально получил.


DAG: визуализация зависимостей

Сейчас у нас DAG из двух нод:

DAG из двух моделей
stg_ordersStaging-модель. View. Source of truth для всех downstream-моделей про заказы.
ref()
orders_largeMart-модель. Table. Зависит от stg_orders через ref().

Это можно увидеть в dbt docs:

dbt docs generate
dbt docs serve

Откроется http://localhost:8080. Найдите orders_large в левой панели, кликните на иконку графа в правом нижнем углу — увидите визуальный lineage.


Цепочка из трёх моделей

Реальные проекты — это цепочки и сети. Добавим intermediate-слой.

models/intermediate/int_orders_with_customers.sql:

SELECT
    o.order_id,
    o.customer_id,
    o.amount,
    o.order_date,
    c.first_name,
    c.last_name
FROM {{ ref('stg_orders') }} o
LEFT JOIN {{ ref('stg_customers') }} c USING (customer_id)

models/marts/orders_revenue.sql:

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

SELECT
    customer_id,
    first_name,
    last_name,
    COUNT(*) AS order_count,
    SUM(amount) AS total_revenue
FROM {{ ref('int_orders_with_customers') }}
GROUP BY 1, 2, 3
ORDER BY total_revenue DESC

И обновлённый stg_customers.sql (вы уже создавали в первом уроке):

SELECT 1 AS customer_id, 'Alice' AS first_name, 'Smith' AS last_name
UNION ALL SELECT 2, 'Bob', 'Jones'
UNION ALL SELECT 3, 'Carol', 'Davis'

Запустим:

dbt run

Вывод (порядок!):

1 of 4 START sql view  model main.stg_customers ................. [RUN]
2 of 4 START sql view  model main.stg_orders .................... [RUN]
1 of 4 OK created sql view  model main.stg_customers ............ [OK in 0.03s]
2 of 4 OK created sql view  model main.stg_orders ............... [OK in 0.03s]
3 of 4 START sql view  model main.int_orders_with_customers ..... [RUN]
3 of 4 OK created sql view  model main.int_orders_with_customers  [OK in 0.04s]
4 of 4 START sql table model main.orders_revenue ................ [RUN]
4 of 4 OK created sql table model main.orders_revenue ........... [OK in 0.05s]

dbt:

  1. Запустил stg_customers и stg_orders параллельно (threads=4, оба не имеют зависимостей)
  2. После того как оба готовы — запустил int_orders_with_customers (зависит от обоих)
  3. После него — orders_revenue (зависит от intermediate)

Это и есть «топологическая сортировка DAG»: параллельность где возможна, последовательность где требуется.


Граф зависимостей

DAG из четырёх моделей
stg_customersStaging: клиенты. Источник для int_orders_with_customers через LEFT JOIN.
stg_ordersStaging: заказы. Источник для int_orders_with_customers.
ref()
int_orders_with_customersIntermediate: JOIN заказов с клиентами. Зависит от обоих staging.
ref()
orders_revenueMart: revenue по клиентам. Table. Final layer для BI.

Что важно понять:

  • int_orders_with_customers имеет два upstream: stg_customers и stg_orders. Оба должны быть готовы.
  • orders_revenue имеет один upstream: int_orders_with_customers. Дальше до stg_* — транзитивно.

dbt вычислил это сам из ref()-вызовов. Никто не писал «orders_revenue depends on int_orders_with_customers».


ref() в Jinja — это функция

ref() — это Jinja-функция (а не специальный SQL-синтаксис). Можно использовать в выражениях:

-- Условный ref в зависимости от target
SELECT * FROM
{% if target.name == 'prod' %}
    {{ ref('orders_prod_only') }}
{% else %}
    {{ ref('orders_dev_only') }}
{% endif %}

Или в цикле:

-- UNION нескольких моделей
{% set models = ['stg_orders_us', 'stg_orders_eu', 'stg_orders_asia'] %}

{% for model in models %}
    SELECT *, '{{ model }}' AS region FROM {{ ref(model) }}
    {% if not loop.last %} UNION ALL {% endif %}
{% endfor %}

При компиляции это развернётся в три SELECT с UNION ALL. И dbt всё ещё построит правильные зависимости — три ребра в DAG, по одному на каждый ref().

TIP

ref() парсится в фазу parse, ещё до выполнения. Если вы пишете {{ ref(some_dynamic_var) }}, где some_dynamic_var зависит от runtime (например, результат run_query()) — dbt не сможет определить зависимость. Используйте ref() со строковым литералом или с переменной, известной на parse.


ref() с двумя аргументами: cross-project

В dbt 1.6+ есть dbt Mesh — возможность ссылаться на модели из другого dbt-проекта:

SELECT * FROM {{ ref('marketing_project', 'campaigns') }}

Первый аргумент — имя другого dbt-проекта (определённого как dependency в packages.yml), второй — модель в нём.

Для junior это за пределами скоупа — упоминаем только для понимания, что увидите в больших корпоративных проектах. Самостоятельно вряд ли понадобится в течение первого года.

dbt Mesh: cross-project ref, model groups и governance

Что НЕ может ref()

Несколько ограничений:

1. ref() работает только для моделей, sources и snapshots

Нельзя сделать {{ ref('some_table_that_was_not_created_by_dbt') }}. Для внешних таблиц (загруженных Fivetran/Airbyte) — нужно использовать {{ source(...) }} (модуль 5 курса).

2. ref() работает только для существующих моделей

Если модель 'X' не существует в проекте — dbt parse упадёт с ошибкой:

Compilation Error in model my_model
  Model 'model.learning_models.my_model' (models/marts/my_model.sql) depends on a node named 'X' which was not found

3. Циклические зависимости запрещены

Если A.sql имеет ref('B'), а B.sql имеет ref('A')dbt parse упадёт:

Compilation Error
  Found a cycle: model.learning_models.A --> model.learning_models.B --> model.learning_models.A

DAG (как название говорит) — Directed Acyclic.

4. ref() парсится на parse-фазе, а не runtime

{% set model_name = "some_value_from_db" %}  -- НЕ работает
SELECT * FROM {{ ref(model_name) }}

Если model_name определяется через run_query() (выполнение SQL во время компиляции), dbt не сможет определить зависимость на parse. Решение — использовать литералы или Jinja-переменные, известные на parse.


ref() vs source()

ref() — для моделей, которые dbt создаёт.

source() — для таблиц, которые в warehouse уже есть и dbt их не создаёт (raw от Fivetran/Airbyte).

-- ref: ссылка на dbt-модель
FROM {{ ref('stg_orders') }}

-- source: ссылка на raw таблицу
FROM {{ source('jaffle_shop', 'orders') }}

Источники объявляются в sources.yml. Подробно — модуль 5 курса.

Принципиально: ref() создаёт зависимость «dbt-модель X зависит от dbt-модели Y». source() создаёт зависимость «dbt-модель X зависит от external-source Z (которую dbt не строит)». Оба попадают в DAG, но обрабатываются по-разному.


Что делает ref() при dbt parse

Чтобы окончательно разобраться: dbt при parse делает примерно следующее (псевдокод Python):

manifest = {"nodes": {}, "edges": []}

for model_file in glob("models/**/*.sql"):
    model_id = basename(model_file).replace(".sql", "")
    sql_content = read_file(model_file)
    
    # Находим все {{ ref('...') }} через Jinja parse
    ref_calls = jinja_parse_refs(sql_content)
    
    # Регистрируем модель и её зависимости
    manifest["nodes"][model_id] = {
        "sql": sql_content,
        "depends_on": [refd_name for refd_name in ref_calls]
    }
    for refd_name in ref_calls:
        manifest["edges"].append((refd_name, model_id))

# Топологическая сортировка
execution_order = topological_sort(manifest["edges"])

# manifest.json записывается в target/
write_json("target/manifest.json", manifest)

То есть весь DAG строится из самого SQL через Jinja-парсинг. Никаких отдельных DAG-файлов, никакой ручной декларации зависимостей. Это и есть фундаментальная идея dbt — код моделей одновременно является и графом.


Попробуй сам

Создайте цепочку из четырёх моделей:

  1. stg_payments.sql (staging) — синтетические оплаты:

    SELECT 1 AS payment_id, 100 AS order_id, 50.00 AS amount
    UNION ALL SELECT 2, 101, 75.50
    UNION ALL SELECT 3, 102, 120.00
  2. int_orders_with_payments.sql (intermediate):

    SELECT
        o.order_id, o.customer_id, o.amount AS order_amount, p.amount AS paid_amount
    FROM `{{ ref('stg_orders') }}` o
    LEFT JOIN `{{ ref('stg_payments') }}` p USING (order_id)
  3. customer_revenue.sql (mart):

    `{{ config(materialized='table') }}`
    SELECT
        customer_id,
        COUNT(*) AS payment_count,
        SUM(paid_amount) AS total_paid
    FROM `{{ ref('int_orders_with_payments') }}`
    GROUP BY 1
  4. Запустите dbt run. Посмотрите порядок выполнения — должен быть stg_* -> int_* -> mart.

  5. Запустите dbt docs generate && dbt docs serve, откройте http://localhost:8080, найдите customer_revenue и посмотрите его lineage — увидите полную цепочку.

  6. Удалите одну модель из ref() — например, int_orders_with_payments. Запустите dbt parse — увидите Compilation Error.


Проверка знанийKnowledge check
У вас есть две модели: A.sql содержит "SELECT * FROM {{ ref('B') }}" и B.sql содержит "SELECT * FROM raw.events". Что dbt сделает при dbt run и в каком порядке?
ОтветAnswer
При dbt parse dbt прочитает оба файла, найдёт {{ ref('B') }} в A.sql и зарегистрирует зависимость: A зависит от B. B не имеет ref(), но имеет жёсткое имя raw.events — это hardcode, не зависимость от dbt-модели. dbt построит DAG: B -> A. При dbt run dbt выполнит модели в топологическом порядке: сначала B (CREATE OR REPLACE VIEW dev.main.B AS SELECT * FROM raw.events), потом A (CREATE OR REPLACE VIEW dev.main.A AS SELECT * FROM dev.main.B). Если raw.events не существует в warehouse — B упадёт на execute-фазе с ошибкой "relation does not exist". Это типичная ошибка джунов — хардкод имени raw-таблицы вместо использования source(). Правильный паттерн — объявить raw.events как source и заменить "FROM raw.events" на "{{ source('raw', 'events') }}". Тогда зависимость от raw была бы в DAG, и freshness можно было бы проверять — но это тема модуля 5.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Что делает Jinja-выражение {{ ref('stg_orders') }} в SQL-модели dbt?

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

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

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

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