DAG как mental model dbt
Если из всего курса нужно вынести одно понятие — это DAG. Все команды dbt, все рассуждения про порядок выполнения, все debug-сценарии — они вокруг DAG.
Этот урок строит ментальную модель: что такое DAG в контексте dbt, как читать lineage, и почему dbt run --select X+ — это центральная команда дев-цикла.
DAG = Directed Acyclic Graph
DAG — это:
- Graph — набор узлов (nodes) и рёбер (edges) между ними
- Directed — рёбра имеют направление: A -> B значит «A до B», не наоборот
- Acyclic — нет циклов: нельзя из A прийти в A по рёбрам, проходя через B, C, D
В dbt:
- Узлы = модели, sources, seeds, snapshots, tests, exposures
- Рёбра = зависимости через
ref()илиsource() - Направление = от parent (upstream) к child (downstream)
Простой пример:
В этом DAG:
fct_orders— lowest in graph (нет downstream)raw.orders— highest in graph (нет upstream)- Направление — сверху вниз (parents -> children)
Upstream и downstream
Базовая терминология:
- Upstream = выше по графу = те, от кого зависит модель
- Downstream = ниже по графу = те, кто зависит от модели
Для int_orders_with_customers:
- Upstream:
stg_orders,stg_customers,raw.orders,raw.customers(всё, что нужно построить раньше) - Downstream:
fct_orders,dim_customers, и так далее (всё, что использует int_orders_with_customers через ref())
Аналогия с течением реки: данные «текут» вниз по графу. Source — родник. Marts — устье.
В docs.getdbt.com upstream иногда называется «parents», downstream — «children». Это синонимы, удобно знать оба.
Node selection: + и @
Selectors + и @ — это синтаксис для выбора частей DAG. Это центральный инструмент дев-цикла.
+ слева — upstream
dbt run --select +fct_orders
«Запусти fct_orders и всё, что нужно построить до него (upstream)».
Это: raw.orders -> stg_orders -> int_orders_with_customers -> fct_orders. Все upstream-модели + сама модель.
+ справа — downstream
dbt run --select stg_orders+
«Запусти stg_orders и всё, что зависит от неё (downstream)».
Это: stg_orders -> int_orders_with_customers -> fct_orders -> fct_revenue -> ....
+ с обеих сторон — both
dbt run --select +int_orders+
«Запусти int_orders_with_customers и всё upstream и downstream».
Это: целая «полоса» DAG, проходящая через эту модель.
N в + ограничивает глубину
dbt run --select +2fct_orders
«fct_orders и 2 уровня upstream» — не дальше.
dbt run --select stg_orders+1
«stg_orders и 1 уровень downstream».
Без числа после + — глубина неограниченная (всё, что есть).
@ — все upstream НО строит только саму модель
dbt run --select @fct_orders
Это reverse-selection: «возьми всё upstream от fct_orders, но строим именно fct_orders». Используется редко в специфических CI-сценариях.
Типичные сценарии node selection
Сценарий 1: Изменил staging-модель, хочу пересобрать всё, что от неё зависит
dbt run --select stg_orders+
Все downstream от stg_orders. Это типичный flow после изменения staging.
Сценарий 2: Нужен fct_orders с нуля, пересоберём всю цепочку
dbt run --select +fct_orders
Все upstream от fct_orders. Полная цепочка построения.
Сценарий 3: CI — запустить только то, что изменено с main
dbt run --select state:modified+ --state target/main
«Модифицированные с main версии + downstream». Это state comparison — модуль 17 курса.
Сценарий 4: Только staging-слой
dbt run --select staging
«Все модели в подпапке staging». Папка интерпретируется как selector.
Сценарий 5: Несколько моделей через запятую (union)
dbt run --select stg_orders stg_customers
«Запусти обе модели и их зависимости».
Сценарий 6: Пересечение через intersection (пробел)
dbt run --select tag:hourly+ tag:revenue
«Модели с тегом hourly + downstream, ПЕРЕСЕЧЕНИЕ с моделями имеющими тег revenue». Подробно в модуле 17.
Что показывает dbt run
При запуске dbt показывает порядок:
1 of 5 START sql view model main.stg_orders ............ [RUN]
2 of 5 START sql view model main.stg_customers ......... [RUN]
1 of 5 OK created sql view model main.stg_orders ....... [OK in 0.04s]
2 of 5 OK created sql view model main.stg_customers .... [OK in 0.04s]
3 of 5 START sql view model main.int_orders ............ [RUN]
3 of 5 OK created sql view model main.int_orders ....... [OK in 0.05s]
4 of 5 START sql table model main.fct_orders ............ [RUN]
5 of 5 START sql table model main.dim_customers ......... [RUN]
4 of 5 OK created sql table model main.fct_orders ....... [OK in 0.06s]
5 of 5 OK created sql table model main.dim_customers .... [OK in 0.05s]
Видим:
- Параллелизм где возможен:
stg_ordersиstg_customersстартанули одновременно (1 of 5 и 2 of 5) - Последовательность где нужно:
int_ordersждал обе staging - Снова параллелизм:
fct_ordersиdim_customersпараллельно (4 of 5 и 5 of 5)
Это и есть топологическая сортировка DAG с учётом threads. dbt вычисляет «какие модели могут запуститься сейчас» и запускает их пачкой до threads.
Топологическая сортировка: что это технически
«Топологическая сортировка» звучит сложно. На самом деле это простой алгоритм:
while есть неиспользованные узлы:
найти все узлы, у которых все upstream уже выполнены
запустить их параллельно (до threads штук)
подождать пока завершатся
отметить как выполненные
Это всё. Для нашего DAG:
- Итерация 1:
stg_orders,stg_customers(нет upstream -> запускаем оба) - Итерация 2:
int_orders(upstream завершены -> запускаем) - Итерация 3:
fct_orders,dim_customers(upstream завершены -> запускаем оба)
dbt делает это автоматически из manifest.json.
Алгоритм гарантирует: модель не запустится, пока её upstream не завершились (даже если есть свободные threads). Это критично для корректности — модель не может прочитать таблицу, которой ещё нет.
Когда DAG ломается: типичные ошибки
1. Forgot to add ref()
Хардкод имени:
-- Неправильно
FROM dev.main.stg_orders
-- Правильно
FROM {{ ref('stg_orders') }}
Симптом: модель работает в dev, но в prod падает (имя dev.main.stg_orders не существует в prod). Или работает везде, но dbt docs serve не показывает зависимость в lineage UI.
2. Forgot to add source()
-- Неправильно
FROM raw.events
-- Правильно
FROM {{ source('raw', 'events') }}
Симптом: dbt не знает про этот source. Freshness не работает. Lineage пустой.
3. Циклическая зависимость
-- A.sql
SELECT * FROM {{ ref('B') }}
-- B.sql
SELECT * FROM {{ ref('A') }}
Симптом на dbt parse:
Compilation Error: Found a cycle: model.X.A --> model.X.B --> model.X.A
Решение: переосмыслить логику, разорвать круг через staging-слой.
4. ref() на несуществующую модель
SELECT * FROM {{ ref('typo_in_name') }}
Симптом:
Compilation Error: Model depends on a node named 'typo_in_name' which was not found
Проверить написание, наличие файла.
5. Запуск без учёта DAG
Команда меняет staging-модель и запускает dbt run --select stg_orders (без +). staging build, но downstream не пересчитан — данные в fct_orders устаревшие. Решение: всегда + справа, когда меняешь что-то в середине DAG.
lineage UI: dbt docs
После dbt docs generate && dbt docs serve открывается интерактивный lineage UI. Что там есть:
- Левая панель — дерево моделей по проектам/папкам
- Центр — описание модели (description, колонки, тесты, конфиги)
- Иконка графа справа внизу — открывает lineage
В lineage:
- Зелёные ноды — модели
- Жёлтые — sources
- Синие — seeds
- Розовые — snapshots
- Иконка теста — рядом с моделью с тестами
Кнопки фильтрации:
+1— показать 1 уровень upstream и downstream+2— 2 уровня-— спрятать узлы фильтрацией
Это полезно на больших проектах (1000+ моделей), где смотреть весь DAG целиком невозможно.
Глобальный DAG vs локальный взгляд
В большом проекте полный DAG не помещается на экран. Поэтому есть два режима работы:
1. Global view (dbt docs serve, или Cosmos/Dagster integrations): видеть весь граф проекта, фильтровать, наводить курсор на ноды для деталей.
2. Local view (dbt CLI + selectors): работать с поднабором — «сейчас я изменил вот эту модель, мне интересны только её downstream».
В дев-цикле большая часть работы — local view. Запустил dbt run --select +my_model+, отладил, ушёл. Global view используется реже — для планирования, дискуссий, ревью больших изменений.
Manifest как «материализованный DAG»
После каждого dbt parse / dbt compile в target/manifest.json оказывается полное JSON-представление DAG:
{
"nodes": {
"model.learning_models.stg_orders": {
"name": "stg_orders",
"depends_on": {
"macros": [],
"nodes": []
},
"config": {"materialized": "view"},
"compiled_code": "SELECT ..."
},
"model.learning_models.fct_orders": {
"name": "fct_orders",
"depends_on": {
"nodes": ["model.learning_models.int_orders"]
},
"config": {"materialized": "table"}
}
},
"sources": {...},
"tests": {...}
}
Это и есть «сериализованный DAG». На его базе работают:
dbt docs serve— рисует lineage из nodes/depends_on- Defer (CI без full rebuild) — сравнивает manifest с prod-версией
- State comparison (
--select state:modified+) — diff manifest.json - Интеграции (Cosmos для Airflow, dbt artifacts package, Elementary) — парсят manifest и кормят свои фичи
Если хотите увидеть граф «как код» — cat target/manifest.json | jq '.nodes | keys' покажет все ноды.
DAG как unit of debugging
Когда что-то не работает — мыслите DAG-ом. Типичный flow:
- fct_orders показывает неправильные данные
- Проверяю compiled SQL fct_orders — он ок
- Смотрю upstream через
dbt list --select +fct_orders— тамint_orders_with_customers - Запускаю
dbt show --select int_orders_with_customers --limit 5— данные ок - Запускаю
dbt show --select stg_orders --limit 5— оп, тут проблема - Понимаю, что баг в staging-слое
Это descent по DAG от mart-уровня к staging. Без понимания DAG этот процесс хаотичный.
Попробуй сам
С моделями из предыдущего урока:
-
Запустите
dbt list --select +fct_orders— должно показать все upstream модели (если у вас цепочка staging -> int -> mart) -
Запустите
dbt list --select stg_customers+— должно показать downstream от stg_customers -
Запустите
dbt run --select stg_orders+1— построит только stg_orders и 1 уровень downstream (а не всю цепочку) -
Откройте
target/manifest.json | jq '.nodes | to_entries[] | {name: .key, depends_on: .value.depends_on.nodes}' | head -50— увидите DAG в виде JSON -
Откройте
dbt docs serve, найдите любую модель, кликните на lineage — увидите визуальный граф -
Создайте искусственную циклическую зависимость: в
stg_ordersдобавьте{{ ref('fct_orders') }}. Запуститеdbt parse— увидите Compilation Error с сообщением про cycle.
DAG в Airflow: та же концепция