В предыдущем модуле мы научились писать модели и связывать их через ref(). Но любой dbt-проект где-то начинается — с raw-таблиц, которые загружает не dbt, а кто-то снаружи: Fivetran, Airbyte, Stitch, скрипт на Python или ручной импорт CSV. Эти таблицы попадают в warehouse, и наши модели должны откуда-то их читать.
Можно было бы написать SELECT * FROM raw.jaffle_shop.customers прямо в SQL-файле модели — и это бы заработало. Но dbt предлагает другой путь: объявить эти raw-таблицы как sources в YAML и обращаться к ним через функцию source(). В этом уроке разбираемся, что такое source, как его декларировать, и зачем вообще эта прослойка.
Source — это не таблица, это декларация таблицы
Главное недоразумение, с которым сталкиваются новички: source ничего не создаёт. Это не модель, не таблица, не view. Source — это просто запись в YAML-файле, которая говорит dbt: “В моём warehouse, в базе raw, в схеме jaffle_shop, есть таблица customers. Я хочу к ней обращаться”.
Сама таблица должна уже существовать в warehouse, попасть туда каким-то внешним процессом. dbt сам её туда не положит — он умеет только читать из неё.
Загрузчик (Fivetran/Airbyte/cron) пишет в raw schema. dbt декларирует эту таблицу как source и читает оттуда. Сам процесс загрузки — вне dbt.
В мире “обычного” SQL мы бы написали FROM raw.jaffle_shop.customers напрямую. В мире dbt — FROM {'{{'} source('jaffle_shop', 'customers') {'}}'}, и эта функция при компиляции подставит то же самое имя. Зачем тогда лишняя обёртка?
Зачем декларировать sources
Есть четыре причины — все они про separation of concerns, разделение зон ответственности.
1. Lineage и DAG. dbt строит граф зависимостей: модель -> ref -> модель -> source. Если sources не задекларированы, начальные узлы графа просто отсутствуют. Это значит, что dbt docs serve не покажет, откуда взялись данные. А на проекте из 200 моделей понять, что зависит от raw.jaffle_shop.customers, становится критично.
2. Переименование без боли. Если завтра DevOps переименует базу raw в landing или сменит схему с jaffle_shop на jaffle, в проекте на чистом SQL придётся менять путь во всех моделях. В dbt — поменяешь один раз в _sources.yml, имя source('jaffle_shop', 'customers') останется тем же.
3. Freshness checks. Можно проверять, не устарела ли таблица: “если последняя запись старше 6 часов — warning, старше суток — error”. Это работает только если таблица задекларирована как source. Подробнее об этом — в третьем уроке.
4. Tests на raw-данных. Можно повесить not_null или unique тест прямо на колонки source, не создавая модель-обёртку. Иногда это полезно, чтобы быстро среагировать на сломанный loader.
Правило большого пальца: никогда не пиши имена raw-таблиц в SQL-моделях напрямую. Всегда оборачивай через source(). Хотя SQL и заработает, ты теряешь lineage, freshness и возможность переименовать. Это одна из самых частых ошибок junior-аналитиков.
Как выглядит _sources.yml
YAML-файл с декларацией обычно лежит в models/staging/<source>/_sources.yml или просто models/_sources.yml для маленьких проектов. Имя файла — конвенция, dbt поднимает любые .yml-файлы в директории models/.
Минимальный пример для Jaffle Shop на DuckDB:
version: 2
sources:
- name: jaffle_shop
database: raw
schema: jaffle_shop
description: "Raw data from Jaffle Shop operational DB, loaded daily by Fivetran."
tables:
- name: customers
description: "One row per customer who ever placed an order."
- name: orders
description: "One row per order. Aggregated across order_items at the warehouse level."
- name: payments
description: "Payments captured via Stripe."
Ключевые поля:
name: jaffle_shop— логическое имя source. Под ним обращаешься в коде:source('jaffle_shop', 'customers').database: raw— физическая база в warehouse. В DuckDB обычно совпадает с именем файла (raw.duckdb-> database =raw), но можно переопределить.schema: jaffle_shop— физическая схема. Если поле опустить, dbt возьмёт schema из profiles.yml (обычноmainдля DuckDB).tables: [...]— список таблиц, которые принадлежат этому source.
Source name vs table name — две разные вещи
Это путает всех на старте. У source есть имя группы (name: jaffle_shop) и имена таблиц внутри (name: customers). В коде ты используешь обе части:
SELECT * FROM {{ source('jaffle_shop', 'customers') }}
Первый аргумент — name группы, второй — name таблицы. Сама физическая таблица в warehouse называется raw.jaffle_shop.customers (database.schema.table), но в dbt-коде ты этого никогда не пишешь — только логические имена.
Source имеет name группы и name таблицы. dbt компилирует source() в полное database.schema.table. Logical names независят от того, где таблица физически лежит.
Можно сделать так, что логическое имя отличается от физического. Например, loader пишет таблицу как cust_2024, но в коде ты хочешь обращаться к ней как к customers:
sources:
- name: jaffle_shop
tables:
- name: customers
identifier: cust_2024
identifier — это физическое имя в warehouse. Если его не указать, dbt берёт name. Аналогично есть database: и schema: на уровне отдельной таблицы — они переопределяют те, что объявлены на уровне source. Полезно, когда одна группа sources разбросана по разным схемам.
Полная иерархия: source -> tables -> columns
YAML может стать большим. Полная структура с описаниями колонок, тестами и метаданными:
version: 2
sources:
- name: jaffle_shop
description: "Raw data from Jaffle Shop operational DB"
database: raw
schema: jaffle_shop
loader: fivetran
loaded_at_field: _fivetran_synced
tables:
- name: customers
description: "One row per customer."
columns:
- name: id
description: "Primary key. UUID generated by app."
tests:
- not_null
- unique
- name: email
description: "Email address. May be empty for guest checkout."
tests:
- not_null
- name: orders
description: "One row per order."
columns:
- name: order_id
tests:
- unique
- not_null
- name: customer_id
tests:
- relationships:
to: source('jaffle_shop', 'customers')
field: id
Каждое поле — опциональное (кроме name на каждом уровне). Тесты можно вешать прямо на source-колонки, и они будут запускаться в dbt test точно так же, как тесты на моделях. Это удобно для контроля за loader-ом: если Fivetran внезапно стал терять id (NULL), тест немедленно упадёт.
Separation of concerns в деталях
Почему вообще такое разделение между loader (Fivetran), raw (его таблицы), decladration (source в YAML) и transformation (модель в dbt)?
Это про разные роли и разные изменения, которые случаются независимо.
Loader отвечает за то, чтобы данные были. Source-декларация описывает интерфейс. Staging-модель чистит и переименовывает. Mart агрегирует. Каждый слой меняется по своим причинам.
Если в одной модели смешать всё (“читаю из raw -> cast -> join -> aggregate”), любое изменение задевает чужие проблемы. Source-декларация изолирует “где лежат raw-данные” от “что я с ними делаю”.
Попробуй сам
В минимальном Jaffle Shop проекте создай файл models/staging/jaffle_shop/_sources.yml со следующим содержимым:
version: 2
sources:
- name: jaffle_shop
database: jaffle_shop
schema: main
tables:
- name: raw_customers
- name: raw_orders
- name: raw_payments
Затем в модели models/staging/jaffle_shop/stg_customers.sql напиши:
SELECT
id AS customer_id,
first_name,
last_name
FROM {{ source('jaffle_shop', 'raw_customers') }}
Запусти dbt run --select stg_customers. dbt скомпилирует source() в полный путь jaffle_shop.main.raw_customers, выполнит SELECT и создаст view (или table — в зависимости от materialization). Если в profiles.yml у тебя path: './jaffle_shop.duckdb', то raw_customers должна уже существовать в этом файле.
Если raw-таблица не существует в warehouse, dbt не создаст её за тебя. Получишь ошибку компиляции вроде Catalog Error: Table with name raw_customers does not exist. Source — это контракт на то, что таблица там УЖЕ есть.
Что мы поняли
Source в dbt — это YAML-декларация существующей в warehouse таблицы. Она даёт четыре вещи: lineage в DAG, изоляцию от переименований, freshness-чеки и возможность вешать тесты на raw-данные. Логическое имя в коде (source('group', 'table')) отделено от физического пути (database.schema.table), что даёт гибкость.
В следующем уроке разберём, как именно функция source() работает в коде, почему она отличается от ref(), и почему категорически нельзя на raw-таблицу делать ref() напрямую.