Learning Platform
Глоссарий Troubleshooting
Урок 06.02 · 16 мин
Начальный
source()ref()Jinjalineageraw tables

В предыдущем уроке мы научились декларировать sources в YAML. Теперь разберём, как именно функция source() работает в SQL-коде, почему её нельзя заменить на ref(), и какие компиляционные ловушки ждут новичка.

Что делает source() при компиляции

source('jaffle_shop', 'customers') — это Jinja-функция. Она ничего не выполняет в runtime, она работает только во время компиляции проекта. dbt берёт два аргумента, ищет в YAML-декларации совпадение, и подставляет полный путь к таблице:

Что происходит при компиляции source()

Jinja-функция source('jaffle_shop', 'customers') читается dbt, ищется в YAML, разворачивается в полный database.schema.table. Никакой runtime магии — обычная text substitution.

Source SQLSELECT * FROM {{ source('jaffle_shop', 'customers') }}
compiledbt при компиляции ищет в _sources.yml блок с name=jaffle_shop, потом среди tables — name=customers. Берёт оттуда database, schema, identifier.
Compiled SQLSELECT * FROM raw.jaffle_shop.customers

Скомпилированный SQL — это то, что dbt отправит в warehouse через адаптер. Никакая Jinja-логика на стороне DuckDB не выполняется. Просто текстовая подстановка.

Если хочешь увидеть результат компиляции — запусти dbt compile и зайди в target/compiled/<project>/models/staging/stg_customers.sql. Там будет обычный SQL без единой {'{{'} ... {'}}'} обёртки.

source() vs ref(): таблица различий

Обе функции — это Jinja, обе разворачиваются в полный путь к таблице. Но они служат разным целям и работают в разном контексте.

source() и ref(): когда какая

ref() для того, что dbt сам создал (моделей, seeds, snapshots). source() — для того, что положил кто-то извне. Использовать неправильную — синтаксис может скомпилироваться, но смысл потеряется.

ref('stg_customers')для моделей, seeds, snapshots
dev_jaffle.stg_customersполный путь после компиляции
source('jaffle_shop', 'customers')для внешних raw-таблиц
raw.jaffle_shop.customersполный путь после компиляции

Главные различия:

  • ref() принимает одно имя (имя модели), source() — два (имя группы и имя таблицы). Это потому что у моделей пространство плоское — имена должны быть уникальны в проекте. У sources может быть несколько таблиц customers, если они из разных источников (jaffle_shop.customers и stripe.customers).
  • ref() строит DAG-зависимость на модель, source() — на источник. dbt планирует порядок выполнения: сначала все sources (хотя их не нужно “выполнять”), потом модели в правильном порядке зависимостей.
  • ref() работает только с тем, что dbt создал сам. Если попытаешься ref('orders') на физическую таблицу, которая создана не dbt, получишь Compilation Error: Model 'model.project.orders' not found.
  • source() работает только с тем, что задекларировано в _sources.yml. Если попытаешься source('foo', 'bar') на незадекларированную таблицу — Source 'foo.bar' not found.

Почему НЕ ref() на raw-таблицу

Самая частая ошибка junior’а — сделать “ленивое” решение и не декларировать sources. Идея: “Я просто напишу в SQL FROM raw.jaffle_shop.customers, и всё заработает”. Технически — да, заработает. Но потеряешь четыре вещи:

1. Lineage сломан. dbt не знает, что stg_customers зависит от raw.jaffle_shop.customers. В dbt docs serve начальный узел графа пропадёт. На большом проекте это означает, что нельзя ответить на вопрос “какие модели сломаются, если loader изменит таблицу?”.

2. Freshness checks невозможны. dbt source freshness работает только на задекларированных sources. Если таблица не в _sources.yml — никаких алертов о её устаревании.

3. Кросс-environment не работает. В dev-окружении схема может называться dev_raw, в prod — prod_raw. С source() ты переключаешь через target.name в YAML или через env_var. С хардкодом — придётся менять SQL.

4. Хардкод имён. Когда DevOps переименует базу, ты будешь искать raw.jaffle_shop.customers по всему проекту через grep. С source() — одно место в YAML.

Не делай так:

-- ПЛОХО: хардкод raw-таблицы
SELECT id, email
FROM raw.jaffle_shop.customers

Сделай так:

-- ХОРОШО: source() с декларацией в YAML
SELECT id, email
FROM {{ source('jaffle_shop', 'customers') }}
DANGER

Никогда не пытайся ref(‘customers’), если customers — это raw-таблица не от dbt. Получишь “Model not found”. А когда добавишь модель stg_customers, начнёшь путаться — ref на staging-модель или на raw. source() vs ref() — это и есть способ не путаться.

Что если source() ссылается на несуществующую таблицу

source() компилируется в полный путь даже если таблица в warehouse не существует. dbt не проверяет существование на этапе компиляции — только при выполнении.

SELECT * FROM {{ source('jaffle_shop', 'nonexistent_table') }}

скомпилируется в:

SELECT * FROM raw.jaffle_shop.nonexistent_table

И только когда DuckDB попытается это выполнить, ты получишь Catalog Error: Table with name nonexistent_table does not exist. Это нормально — source — это контракт на то, что таблица существует, и dbt доверяет YAML-декларации.

Если хочешь проверить существование заранее — есть команда dbt source freshness, которая для своей работы делает SELECT MAX(loaded_at_field) и упадёт с понятной ошибкой, если таблица недоступна.

Полная анатомия скомпилированного запроса

Возьмём типичную staging-модель и посмотрим, во что она разворачивается.

Исходная модель models/staging/stg_customers.sql:

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

WITH source AS (
    SELECT * FROM {{ source('jaffle_shop', 'raw_customers') }}
),

renamed AS (
    SELECT
        id        AS customer_id,
        first_name,
        last_name
    FROM source
)

SELECT * FROM renamed

После dbt compile в target/compiled/jaffle_shop/models/staging/stg_customers.sql:

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

Обрати внимание:

  • {'{{'} config(...) {'}}'} блок исчез — он не часть SQL, это инструкция для dbt о том, как материализовать модель.
  • {'{{'} source(...) {'}}'} развернулся в полный путь jaffle_shop.main.raw_customers.
  • В target/run/.../stg_customers.sql будет ещё одна обёртка — CREATE OR REPLACE VIEW ... AS (...). Это то, что dbt реально отправит в DuckDB.

DuckDB-специфика для source()

В DuckDB sources могут указывать не только на таблицы, но и на внешние файлы через специальные конструкции. Например, parquet/csv-файлы можно подключить так:

sources:
  - name: raw_lake
    meta:
      external_location: "read_parquet('./data/{name}.parquet')"
    tables:
      - name: customers
      - name: orders

Когда такая декларация подхвачена расширением dbt-duckdb, source(‘raw_lake’, ‘customers’) скомпилируется в read_parquet('./data/customers.parquet') вместо обычной таблицы. Это уникальная DuckDB-фича — на Snowflake/BigQuery такого нет. Но для junior-курса мы остановимся на классических таблицах.

NOTE

external_location — это специфика dbt-duckdb адаптера, в стандартном dbt-core нет. Удобно для прототипов с Jaffle Shop, где raw-данные лежат в CSV/parquet. На прод используется только если warehouse реально DuckDB; на других warehouse — обычные таблицы.

Команды для отладки source()

Три полезные команды, когда что-то идёт не так:

# Скомпилировать без выполнения. Посмотришь, во что развернулся source()
dbt compile --select stg_customers

# Показать список всех задекларированных sources в проекте
dbt list --resource-type source

# Проверить, что source доступен (выполняет SELECT MAX(loaded_at))
dbt source freshness --select source:jaffle_shop

dbt list --resource-type source особенно полезен, когда ты унаследовал проект с десятками YAML-файлов и хочешь понять, что вообще задекларировано:

$ dbt list --resource-type source
source:jaffle_shop.customers
source:jaffle_shop.orders
source:jaffle_shop.payments
source:stripe.charges
source:stripe.subscriptions

Попробуй сам

Возьми свой минимальный Jaffle Shop проект. Скомпилируй модель:

dbt compile --select stg_customers

Открой файл target/compiled/jaffle_shop/models/staging/stg_customers.sql. Найди строку с FROM и убедись, что там полный путь к таблице, а не Jinja-обёртка.

Теперь “сломай” что-нибудь, чтобы прочувствовать ошибки:

  1. Закомментируй блок tables в _sources.yml, оставь source без таблиц. Запусти dbt compile — получишь Source 'jaffle_shop.raw_customers' not found.
  2. Поменяй в YAML database: raw на database: nonexistent. Запусти dbt run — получишь Catalog Error при выполнении (но компиляция пройдёт, потому что dbt доверяет YAML).
  3. Запусти dbt list --resource-type source, посмотри, что в твоём проекте задекларировано.

Что мы поняли

source() — это Jinja-функция, которая при компиляции разворачивается в полный путь к таблице. Она работает только с задекларированными в _sources.yml записями, и она принципиально отличается от ref(): ref для того, что создал dbt, source — для внешних данных. Использовать “сырое” FROM raw.schema.table без декларации можно технически, но это ломает lineage, freshness и кросс-environment работу. В следующем уроке разберём, как использовать sources для проверки свежести данных через freshness checks.

Ingestion: откуда берутся raw-таблицы
Проверка знанийKnowledge check
Ты написал в модели stg_orders запрос FROM {'{{'} ref('orders') {'}}'}, имея в виду raw-таблицу orders из Fivetran. dbt run падает с "Model 'model.jaffle_shop.orders' not found". Что не так и как починить?
ОтветAnswer
ref() работает только с моделями/seeds/snapshots, которые dbt создал сам. raw-таблица orders загружается извне (Fivetran), значит это source, а не ref. Нужно: (1) задекларировать её в _sources.yml с name группы (например jaffle_shop) и tables: [orders], (2) в SQL использовать FROM {'{{'} source('jaffle_shop', 'orders') {'}}'}. После этого dbt построит правильный lineage и компиляция пройдёт.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Чем функция source() в Jinja отличается от ref()?

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

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

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

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