Selector-based runs: state:modified, result:error, source_status:fresher
В крупном проекте полный dbt run может занимать час, а на CI это ещё умножается на количество PR. Запускать всё ради изменения одного staging-models — расточительство compute. Selectors дают возможность запустить только то, что нужно — изменённое, упавшее, или то, у чего обновились данные.
Этот урок — про production-применение state:modified+, result:error+, source_status:fresher+ и композиции selectors для CI optimization.
Базовый синтаксис selectors
В dbt-core selectors задаются через флаг --select (или -s):
dbt run --select model_name # одна модель
dbt run --select stg_orders+ # модель + все downstream
dbt run --select +fct_orders # модель + все upstream
dbt run --select +fct_orders+ # модель + upstream + downstream
dbt run --select tag:hourly # все модели с тегом hourly
dbt run --select path:models/staging # все модели в директории
Это базовая семантика. Operators + означают «и зависимости в этом направлении», tag:, path: — это methods (поиск по атрибуту).
Senior-уровень — state-based selectors, которые сравнивают текущий код с эталонным manifest’ом.
state:modified+ — slim CI
Slim CI — это паттерн: на каждом PR запускать только изменённые модели (и их downstream), не весь проект. Это даёт 10-100x экономию compute.
Механика:
- У вас есть
manifest.jsonот prod (последний successful run). - На CI вы делаете
dbt parseдля текущей ветки -> новый manifest. state:modified+сравнивает два manifest-а и возвращает изменённые модели + downstream.
# CI script
dbt run --select state:modified+ --state target/prod_manifest/ --defer
Что значит --state target/prod_manifest/:
- Папка содержит
manifest.jsonот последнего prod-run. - dbt парсит свой код, сравнивает с этим manifest.
Что значит --defer:
- Для refs на unchanged модели — используется их prod-версия (через
database.schema.tableиз state). - Не нужно билдить весь upstream — только модифицированные модели + downstream видят upstream через prod.
Изменена одна модель stg_orders. dbt с state:modified+ построит её + всё, что зависит. Остальное — defer to prod.
Это даёт огромный выигрыш — на PR, который меняет одну staging-модель, dbt билдит 3 модели вместо 1000.
Как работает «modified» в state-comparison
dbt смотрит на конкретные поля манифеста для определения modified. Главные:
raw_code— SQL-код модели. Изменился — modified.config— настройки модели (materialized, unique_key, partition_by). Изменились — modified.description— документация модели. Это тоже триггерит modified! (Спорное поведение, но дефолтное.)columns— список колонок в schema.yml.refs/sources— список зависимостей.macros— компиляция использует macro X, X изменился — модель considered modified.
Полный список: state:modified.body (только raw_code), state:modified.configs, state:modified.macros, state:modified.contract и т.д. По умолчанию state:modified — union всех.
Самый коварный сценарий: добавили строку документации в schema.yml для модели X. dbt считает X modified, билдит её и весь downstream. На большой DAG это значит, что простой PR с doc updates стартует часовой CI run. Решение — использовать более узкий selector state:modified.body (только реальные code changes), или исключить doc-only PRs из CI через path-checking.
Тонкость: чтобы исключить doc updates, иногда используют:
dbt run --select 'state:modified.body,state:modified.configs,state:modified.macros'
Это только code/config/macro changes. Документация исключена.
—state path: где брать prod manifest
Это критический вопрос для CI setup. Есть три паттерна:
- Скачивать prod manifest на каждом CI: prod artifact storage (S3, Azure Blob), на CI делаете
aws s3 cp s3://bucket/prod-manifest/manifest.json target/prod_manifest/. Это рекомендуемый подход. - dbt Cloud feature: dbt Cloud сохраняет manifests от scheduled runs автоматически. На CI можно через API получить.
- Git-versioned manifest: коммитить
manifest.jsonв репозиторий. Антипаттерн — большой бинарный файл, постоянные конфликты.
Реальный CI script с GitHub Actions:
# .github/workflows/dbt-ci.yml
- name: Download prod manifest
run: |
aws s3 cp s3://my-dbt-state/prod/manifest.json target/prod_manifest/manifest.json
- name: dbt parse
run: dbt parse
- name: dbt build modified
run: dbt build --select state:modified+ --state target/prod_manifest/ --defer
result:error+1 — retry на failed моделях
После dbt run у вас есть target/run_results.json (см. модуль 05). Он содержит status каждой модели: success, error, skipped. Selector result:error+1 означает “все упавшие модели + ничего глубже”:
# Первый run — что-то упало
dbt run
# Retry только failed
dbt run --select result:error+1 --state target/
# +1 — означает один уровень downstream от failed (на случай если downstream ждал error model)
Реальный use case — flaky tests или transient errors:
# CI script с retry
dbt run --select state:modified+ --state target/prod_manifest/ --defer
if [ $? -ne 0 ]; then
echo "Some models failed, retrying..."
dbt run --select result:error+1 --state target/
fi
Это снижает false failures на CI из-за transient warehouse errors (например, connection timeouts).
Тонкость: result:error+1 vs result:error+. +1 — один уровень downstream. + — все downstream. На retry обычно нужен только +1, потому что глубже модели не успели стартовать, для них ничего нет в run_results.
Аналогично есть:
result:skipped— модели, скипнутые из-за upstream error.result:success— успешные (редко нужен).result:fail— failed tests (дляdbt test).
source_status:fresher+ — freshness-driven runs
Это менее известный, но мощный selector. Идея: запускать модели только когда источники обновились.
dbt имеет dbt source freshness команду, которая проверяет, насколько свежие данные в источниках:
# sources.yml
sources:
- name: app
tables:
- name: orders
loaded_at_field: updated_at
freshness:
warn_after: { count: 1, period: hour }
error_after: { count: 6, period: hour }
Запускаете:
dbt source freshness
Это создаёт target/sources.json с freshness статусами. Дальше:
# Запустить модели, у которых источники обновились
dbt run --select source_status:fresher+ --state target/
fresher+ означает «источник свежее, чем в state» + все downstream.
Production-сценарий — adaptive scheduling:
# Каждые 15 минут: проверка freshness
dbt source freshness
# Если хоть один источник обновился — run downstream
dbt run --select source_status:fresher+ --state target/
Это позволяет реагировать на свежие данные мгновенно, не тратя compute на пустой run, когда ничего не пришло.
Композиция selectors
Selectors можно комбинировать. Это даёт мощные паттерны:
AND через запятую (intersection)
dbt run --select 'tag:hourly,state:modified+'
— модели с тегом hourly И одновременно modified.
OR через пробелы (union)
dbt run --select 'state:modified+' 'source_status:fresher+'
— modified models OR fresher sources downstream.
Exclude
dbt run --select state:modified+ --exclude tag:experimental
— modified models, кроме тех с тегом experimental.
Полный пример CI
dbt run \
--select 'state:modified+' 'source_status:fresher+' \
--exclude tag:expensive_skip_on_pr \
--state target/prod_manifest/ \
--defer
Это: rebuild modified models + downstream fresher sources, исключая дорогие experimental модели.
State methods глубже
Кроме state:modified есть менее известные:
| Selector | Семантика |
|---|---|
state:new | Модели, которых не было в state (новые) |
state:old | Модели, которые были в state, но удалены в текущем коде |
state:modified | Изменённые (любым из критериев) |
state:modified.body | Только raw_code |
state:modified.configs | Только config changes |
state:modified.contract | Только contract changes (см. dbt-ii контракты) |
state:modified.macros | Только зависящие macros изменились |
state:unmodified | Не изменены (редко нужно) |
state:new особенно полезен — после создания новой модели на CI она должна быть build даже без upstream dependencies. state:modified+ это покрывает (новая модель = modified), но иногда хочется только новые: dbt run --select state:new.
defer без state:modified
--defer без selectors тоже работает. Это значит: «билдь то, что я попросил, для остального ref’ы возьми из state»:
dbt run --select fct_user_metrics --defer --state target/prod_manifest/
Здесь dbt билдит только fct_user_metrics, а её upstream-зависимости берёт из prod через defer. Это полезно для ad-hoc rebuild одной модели в prod-like environment.
Snowflake/BigQuery: cost-aware CI
На облачных warehouses каждый CI run стоит реальные деньги. С Slim CI можно сэкономить 90%:
| Setup | CI run time | Compute cost (Snowflake Medium $4/hr) |
|---|---|---|
| Full rebuild | 60 min | $4.00 per PR |
| Slim CI (state:modified+) | 5 min | $0.33 per PR |
| Slim CI + defer | 3 min | $0.20 per PR |
На команде из 20 разработчиков с 5 PR-ов в день каждый — это разница в $7,200/мес.
DuckDB: на CI с DuckDB cost ноль (локальный compute), но time экономия всё равно важна — PR feedback должен быть быстрый.
Failure modes
Production gotchas:
-
prod manifest stale — забыли обновить артифакт в S3, на CI старый manifest.
state:modifiedпоказывает кучу false positives. Решение — обновлять manifest на каждом prod-run (черезon-run-endhook). -
Schema drift в —defer — defer ссылается на
prod.schema.table, а вы в текущем PR поменяли columns. dbt предполагает, что схема такая же — build модели может сломаться runtime, потому что upstream имеет другие колонки. -
macros не считаются upstream через defer — если вы поменяли macro X, который используется в модели Y (не модифицированной), Y попадёт в
state:modifiedчерезstate:modified.macros. Это иногда отлавливает edge cases. -
Result selector без state —
result:error+1без--state target/не работает. dbt не знает, где run_results. -
packages не поддерживают state: если внутри package есть модели, и package обновлён, dbt пересчитывает их все. Это иногда триггерит большие rebuilds при
dbt deps update.
Самый болезненный production-инцидент: forgot --defer, CI build падает с «Relation analytics.PROD.stg_orders does not exist». Это потому что state:modified+ пытается ref’ить unchanged staging, которое в dev-schema не существует. Решение — всегда --defer для CI runs.
Mock CI sequence
Типичный полноценный CI run выглядит так:
# 1. Setup
git checkout origin/main -- target/prod_manifest/ # или скачать из S3
# 2. Parse — generate manifest для текущего PR
dbt parse
# 3. Source freshness (optional)
dbt source freshness
# 4. Build modified + fresher
dbt build \
--select 'state:modified+' 'source_status:fresher+' \
--state target/prod_manifest/ \
--defer
# 5. Retry on flaky errors
if [ $? -ne 0 ]; then
sleep 30
dbt build --select result:error+1 --state target/
fi
# 6. Tests on changed (uncached)
dbt test --select 'state:modified,test_type:singular' --state target/prod_manifest/
# 7. Upload new manifest для следующего CI
aws s3 cp target/manifest.json s3://my-dbt-state/ci/$PR_ID/manifest.json
Резюме
state:modified+— основа Slim CI. Сравнивает с prod-manifest, билдит только изменённое + downstream.--deferдополняет — для unchanged upstream использует prod-таблицы.state:modified.body— узкий вариант (исключает doc updates).result:error+1— retry упавших моделей.source_status:fresher+— freshness-driven runs.- Композиция через запятые (AND), пробелы (OR),
--exclude. - Production cost: Slim CI экономит 80-95% compute на больших проектах.
- Gotchas: prod manifest stale, schema drift на defer, packages не в state.