Learning Platform
Глоссарий Troubleshooting
Урок 06.01 · 18 мин
Начальный
sources_sources.ymlraw dataseparation of concerns

В предыдущем модуле мы научились писать модели и связывать их через 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 сам её туда не положит — он умеет только читать из неё.

Откуда берутся raw-таблицы и куда смотрит dbt

Загрузчик (Fivetran/Airbyte/cron) пишет в raw schema. dbt декларирует эту таблицу как source и читает оттуда. Сам процесс загрузки — вне dbt.

Fivetran / Airbyte / cron-скриптвнешний loader
raw.jaffle_shop.customersфизическая таблица в warehouse
sources YAMLdbt декларирует и читает

В мире “обычного” 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.

TIP

Правило большого пальца: никогда не пиши имена 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-коде ты этого никогда не пишешь — только логические имена.

Логические имена в коде vs физический путь

Source имеет name группы и name таблицы. dbt компилирует source() в полное database.schema.table. Logical names независят от того, где таблица физически лежит.

В кодеsource('jaffle_shop', 'customers')
После компиляцииraw.jaffle_shop.customers
name группыname: jaffle_shop в YAML — это логическое имя группы. Его выбираешь ты, оно может совпадать со схемой или нет.
name таблицыname: customers — физическое имя таблицы в warehouse. Должно точно совпадать с тем, как loader её записывает.
полный путьdatabase+schema+table собирается dbt автоматически из database/schema полей source и name таблицы.

Можно сделать так, что логическое имя отличается от физического. Например, 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)?

Это про разные роли и разные изменения, которые случаются независимо.

Слои ответственности от raw до mart

Loader отвечает за то, чтобы данные были. Source-декларация описывает интерфейс. Staging-модель чистит и переименовывает. Mart агрегирует. Каждый слой меняется по своим причинам.

Layer 1: LoaderFivetran/Airbyte/Python script
changesМеняется когда: добавляется новый источник, меняется частота загрузки, ломается коннектор.
Layer 2: Source declaration_sources.yml
changesМеняется когда: переименовали базу/схему, добавили новую таблицу из того же loader, изменилось поле loaded_at.
Layer 3: Staging modelstg_jaffle__customers.sql
changesМеняется когда: aliasing колонок, фильтр на удалённые записи, cast типов, добавили surrogate key.
Layer 4: Martdim_customers.sql
changesМеняется когда: новый бизнес-метрик, изменение определения customer lifetime value, новые dimensions.

Если в одной модели смешать всё (“читаю из 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 должна уже существовать в этом файле.

WARNING

Если 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() напрямую.

Ingestion: как raw-данные попадают в warehouse до того, как dbt их читает
Проверка знанийKnowledge check
Ты добавил в _sources.yml новую таблицу orders, но dbt в run падает с "Catalog Error: Table with name orders does not exist". Что не так?
ОтветAnswer
Source в dbt — это только декларация. Сама физическая таблица должна уже существовать в warehouse, dbt её не создаёт. Если её там нет, нужно либо загрузить данные через внешний loader (Fivetran, скрипт), либо проверить database/schema/identifier в YAML — возможно, имена не совпадают с тем, где таблица реально лежит. Source НЕ материализует данные.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Что делает декларация source в _sources.yml физически в warehouse?

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

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

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

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