Slim CI: state:modified+ deep dive
«Slim CI» — это паттерн запуска dbt в CI: вместо полного dbt build (который пересчитывает все 200 моделей за 30 минут) запускать только изменённые модели и их downstream за 2-3 минуты. Экономия времени и compute огромная.
Технически Slim CI это две связанные фичи dbt: selector state:modified+ (запустить изменённые) и --defer (использовать prod таблицы для unchanged зависимостей). В этом уроке разбираем первую часть, защиту прода через state:modified. Defer — следующий урок.
Что делает state:modified+
dbt build \
--select state:modified+ \
--state ./prod-state/
dbt сравнивает текущий target/manifest.json (после parse) с указанным --state (prod manifest). По разнице checksum каждой модели определяет: какая изменилась. Получает список modified nodes.
+ в конце — это DAG операторы: «эта модель и все её потомки в DAG».
DAG операторы: +, @, n+, +n
state:modified+ — это один из вариантов. Полная таблица операторов:
| Селектор | Что значит |
|---|---|
state:modified | Только изменённые модели. Без зависимостей. |
state:modified+ | Изменённые + ВСЕ их downstream (рекурсивно). |
state:modified+1 | Изменённые + только 1 уровень downstream. |
+state:modified | Изменённые + ВСЕ их upstream. |
1+state:modified | Изменённые + только 1 уровень upstream. |
+state:modified+ | Изменённые + ВСЕ upstream И ВСЕ downstream. |
@state:modified | Изменённые + ВСЕ upstream + ВСЕ downstream И downstream upstream-моделей. |
В Slim CI обычно используется state:modified+ — нужно проверить что изменения не сломали downstream. Upstream обычно не запускают (он не изменился, и его проверять не надо).
Что значит «modified» — детально
dbt считает модель modified если изменилось ЛЮБОЕ из:
- Содержимое SQL (
raw_codechecksum). Изменилось даже на пробел — да. - Конфиг модели (
config). Изменился materialized, schema, tags, и т.д. - Контракт (
contractblock). - Колонки и метаданные в
_models.yml. - Тесты модели.
Чисто иллюстративно:
-- models/staging/stg_customers.sql
-- Если просто добавить пробел:
SELECT id, name FROM customers -- было: SELECT id, name FROM customers
-- -> checksum изменился -> state:modified включает stg_customers
Этим обеспечивается «всё что технически изменилось — будет проверено».
state:modified.body vs state:modified.configs
В dbt 1.7+ можно фильтровать по конкретному типу изменения:
| Селектор | Что включается |
|---|---|
state:modified.body | Изменения SQL текста |
state:modified.configs | Изменения в config()/dbt_project.yml configs |
state:modified.relation | Изменения в databasename/schema/identifier |
state:modified.persisted_descriptions | Изменения в descriptions, которые persist в warehouse |
state:modified.macros | Изменения в макросах модели |
state:modified.contract | Изменения в model contracts |
Можно комбинировать:
# Запустить только если изменился SQL или config (не documentation-only changes)
dbt build --select state:modified.body+ state:modified.configs+ --state ./prod/
В типичном CI используют простой state:modified+ — он включает всё. Гранулярные модификаторы — для специальных случаев.
Полный workflow Slim CI
# .github/workflows/dbt-ci.yml
name: dbt CI
on:
pull_request:
branches: [main]
jobs:
slim-ci:
runs-on: ubuntu-latest
env:
DBT_PROFILES_DIR: .
SNOWFLAKE_ACCOUNT: ${'{{'} secrets.SNOWFLAKE_ACCOUNT {'}}'}
SNOWFLAKE_USER: ${'{{'} secrets.SNOWFLAKE_USER {'}}'}
SNOWFLAKE_PASSWORD: ${'{{'} secrets.SNOWFLAKE_PASSWORD {'}}'}
DBT_PR_NUMBER: ${'{{'} github.event.pull_request.number {'}}'}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install dbt
run: pip install dbt-snowflake==1.10.0
- name: dbt deps
run: dbt deps
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${'{{'} secrets.AWS_ACCESS_KEY_ID {'}}'}
aws-secret-access-key: ${'{{'} secrets.AWS_SECRET_ACCESS_KEY {'}}'}
aws-region: us-east-1
- name: Download prod manifest
run: |
mkdir -p ./prod-state/
aws s3 cp s3://my-dbt-state/prod/manifest.json ./prod-state/manifest.json
- name: Check manifest freshness
run: |
python -c "
import json, datetime
with open('./prod-state/manifest.json') as f:
m = json.load(f)
gen = datetime.datetime.fromisoformat(m['metadata']['generated_at'].replace('Z', '+00:00'))
age = datetime.datetime.now(datetime.timezone.utc) - gen
if age > datetime.timedelta(days=2):
raise Exception(f'Manifest is {age.days} days old')
"
- name: dbt parse
run: dbt parse --target ci
- name: Slim CI build
run: |
dbt build \
--target ci \
--select state:modified+ \
--defer \
--state ./prod-state/ \
--fail-fast
Разбор по шагам:
- Setup Python + install dbt — стандартно.
- dbt deps — установка зависимостей dbt.
- Download prod manifest — из S3 (или artifact, или Pages).
- Check freshness — manifest не должен быть старше 2 дней (защита от stale).
- dbt parse — генерирует свой manifest для текущей feature branch. Без этого dbt не знает что сравнивать.
- Slim CI build —
state:modified+ --defer --state. Запускает diff + downstream.
—fail-fast: быстрый exit при первой ошибке
dbt build --select state:modified+ --fail-fast
Без --fail-fast dbt пытается запустить все модели в DAG, даже если упстрим упал. Каждая dependent модель упадёт с error: upstream failed.
С --fail-fast — на первой неисправной модели весь run останавливается. В CI это экономит минуты и даёт более чистый error report.
В CI всегда используйте --fail-fast. Если 50 моделей зависят от сломанного stg_customers — нет смысла видеть 50 одинаковых ошибок. Падаем сразу, разработчик чинит первопричину.
Schema isolation: где запускать PR build
Slim CI запускает изменённые модели на изолированной схеме, не на prod. Стандартный паттерн — схема pr_<PR_NUMBER>:
# profiles.yml
ci:
type: snowflake
database: ${'{{'} env_var("SNOWFLAKE_DATABASE") {'}}'}
schema: pr_${'{{'} env_var("DBT_PR_NUMBER") {'}}'} # ключевая строка
...
В CI workflow DBT_PR_NUMBER приходит из github.event.pull_request.number.
После build:
- В warehouse появилась
analytics.pr_123.stg_customers,analytics.pr_123.fct_orders. - Production таблицы (
analytics.prod.stg_customers) не тронуты.
Cleanup после PR (см. урок 1 этого модуля) удаляет схему pr_123 чтобы не плодились заброшенные.
custom_schema через generate_schema_name
В реальном проекте schema берётся через generate_schema_name macro:
{% macro generate_schema_name(custom_schema_name, node) -%}
{%- set default_schema = target.schema -%}
{%- if target.name == 'prod' and custom_schema_name -%}
{{ custom_schema_name | trim }}
{%- elif target.name == 'ci' -%}
{{ default_schema }} -- pr_<num>, без custom_schema суффикса
{%- else -%}
{{ default_schema }}_{{ custom_schema_name | trim }}
{%- endif -%}
{%- endmacro %}
Это держит CI clean — все модели в одной схеме pr_123, не разрастающейся через custom_schema.
Когда Slim CI не работает
Slim CI зависит от manifest comparison. Есть случаи когда механизм не покрывает изменения:
1. Изменения в seeds
seeds/raw_currency.csv # обновили курсы валют
state:modified смотрит на модели, не на seeds. Если seed изменился — Slim CI его пропустит. Решение — отдельный селектор:
dbt build --select state:modified+ source:* seeds.modified+
Точный синтаксис зависит от версии dbt. В 1.10+ есть state:modified для seeds.
2. Изменения в макросах
Если изменился generic macro, он может влиять на все модели — но diff покажет только модель которая ИСПОЛЬЗУЕТ макрос (через изменённый compiled SQL). Иногда compiled SQL не меняется (macro changes invisible).
В dbt 1.10+ есть state:modified.macros для отслеживания.
3. Изменения в packages.yml
Новый package или обновлённая версия — Slim CI это не подхватит. Это infrastructural change, требует full rebuild:
# В workflow:
- name: Check packages.yml change
id: pkg-check
run: |
if git diff origin/main...HEAD --name-only | grep -q packages.yml; then
echo "full-build=true" >> $GITHUB_OUTPUT
fi
- name: Full build (packages changed)
if: steps.pkg-check.outputs.full-build == 'true'
run: dbt build --target ci
- name: Slim CI build
if: steps.pkg-check.outputs.full-build != 'true'
run: dbt build --target ci --select state:modified+ --defer --state ./prod-state/
4. dbt version upgrade
При апгрейде dbt 1.10 -> 1.11 структура manifest.json может измениться -> diff врёт. Решение — после upgrade сделать один full rebuild prod, обновить prod manifest, потом возвращаться к Slim CI.
Метрики Slim CI
Что можно мерить:
- CI время до и после Slim CI. Типично с 30 мин -> 3-5 мин (на проекте 200 моделей).
- Cost reduction. На Snowflake — линейно сэкономленному времени warehouse runtime.
- Coverage. Процент PR-ов которые проходят через Slim CI (vs full rebuild fallback). Если меньше 80% — что-то не так с manifest pipeline.
В реальных проектах эти метрики экспортируются в Datadog / Grafana для мониторинга.
Сравнение: full build vs Slim CI
Full build на проекте 200 моделей:
- Время: 30 минут
- Compute (Snowflake): ~5 кредитов
- Каждый PR: 5 кредитов
Slim CI на том же проекте, типичный PR с 3 изменёнными моделями:
- Время: 3 минуты (3 modified + 10 downstream = 13 моделей)
- Compute: ~0.5 кредита
- Каждый PR: 0.5 кредита
Месяц: 200 PR
- Full build: 1000 кредитов
- Slim CI: 100 кредитов
- Экономия: 90% компьюта
На enterprise команде эта экономия — десятки тысяч долларов в год.
Попробуй сам
В вашем dbt-проекте с DuckDB:
- Сделайте
dbt build --target devи сохранитеtarget/manifest.jsonкак «production»:
dbt build --target dev
cp target/manifest.json ./prod-state/manifest.json
- Измените одну модель, например
models/staging/stg_customers.sql:
-- Добавьте новую колонку
SELECT
id,
name,
email,
UPPER(name) AS name_upper -- новая
FROM {'{{ source(...) }}'}
- Запустите Slim CI команду:
dbt parse
dbt ls --select state:modified --state ./prod-state/
Увидите только stg_customers (модель которую вы изменили).
- С
+:
dbt ls --select state:modified+ --state ./prod-state/
Увидите stg_customers и все downstream модели (если есть).
- Запустите build:
dbt build --select state:modified+ --state ./prod-state/ --target dev
Запустятся только эти модели.
Бонус: попробуйте state:modified.body+ (только body changes) и state:modified.configs+. Сравните результаты.
Ключевые выводы
- Slim CI — паттерн запуска dbt CI на изменённых моделях вместо full build. Технически —
state:modified+ --defer --state. - state:modified — селектор, который сравнивает текущий manifest с
--statemanifest. Включает все модели где изменился checksum (SQL, config, contract, etc.). - DAG операторы:
+(downstream),+префикс (upstream),n+(N уровней). В Slim CI обычноstate:modified+(изменённые + downstream). - state:modified.body / .configs / .macros — гранулярные модификаторы для специальных случаев.
- —fail-fast в CI — обязательно. Не плодит ошибки upstream-fail на 50 моделях.
- Schema isolation через
pr_<NUMBER>schema. Cleanup после PR обязателен (см. урок 1). - Gotchas: изменения в seeds/macros/packages.yml могут быть пропущены Slim CI. Для критичных случаев — fallback на full build.
- Экономия compute: типично 80-95% по сравнению с full build. На enterprise команде — десятки тысяч в год.