Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 14.03 · 30 мин
Средний
Slim CIstate:modifiedDAG selectorsdefer

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 — следующий урок.

Airflow: idempotency и targeted re-runs — аналогичный принцип

Что делает state:modified+

dbt build \
  --select state:modified+ \
  --state ./prod-state/

dbt сравнивает текущий target/manifest.json (после parse) с указанным --state (prod manifest). По разнице checksum каждой модели определяет: какая изменилась. Получает список modified nodes.

+ в конце — это DAG операторы: «эта модель и все её потомки в DAG».

state:modified+ — что включается
Шаг 1: dbt вычисляет diff между двумя manifestsСравнение через sha256 hash от raw_code (SQL текста) + config + columns. Если hash отличается от prod manifest — модель modified.
Шаг 2: получает список изменённых нодРезультат — список modified nodes. Например: [stg_customers, fct_orders, dim_customers].
Шаг 3: + оператор добавляет downstreamСелектор + добавляет все downstream модели. Если изменился stg_customers, в список добавляются все модели которые от него зависят: int_customer_metrics -> fct_orders -> mrt_customer_summary.
Шаг 4: запускает выбранный subsetФинальный selection — список моделей для запуска. Только то что изменилось ИЛИ зависит от изменённого. Остальные модели не трогаются.

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 если изменилось ЛЮБОЕ из:

  1. Содержимое SQL (raw_code checksum). Изменилось даже на пробел — да.
  2. Конфиг модели (config). Изменился materialized, schema, tags, и т.д.
  3. Контракт (contract block).
  4. Колонки и метаданные в _models.yml.
  5. Тесты модели.

Чисто иллюстративно:

-- 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

Разбор по шагам:

  1. Setup Python + install dbt — стандартно.
  2. dbt deps — установка зависимостей dbt.
  3. Download prod manifest — из S3 (или artifact, или Pages).
  4. Check freshness — manifest не должен быть старше 2 дней (защита от stale).
  5. dbt parse — генерирует свой manifest для текущей feature branch. Без этого dbt не знает что сравнивать.
  6. Slim CI buildstate: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.

TIP

В 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:

  1. Сделайте dbt build --target dev и сохраните target/manifest.json как «production»:
dbt build --target dev
cp target/manifest.json ./prod-state/manifest.json
  1. Измените одну модель, например models/staging/stg_customers.sql:
-- Добавьте новую колонку
SELECT
  id,
  name,
  email,
  UPPER(name) AS name_upper   -- новая
FROM {'{{ source(...) }}'}
  1. Запустите Slim CI команду:
dbt parse
dbt ls --select state:modified --state ./prod-state/

Увидите только stg_customers (модель которую вы изменили).

  1. С +:
dbt ls --select state:modified+ --state ./prod-state/

Увидите stg_customers и все downstream модели (если есть).

  1. Запустите build:
dbt build --select state:modified+ --state ./prod-state/ --target dev

Запустятся только эти модели.

Бонус: попробуйте state:modified.body+ (только body changes) и state:modified.configs+. Сравните результаты.


Ключевые выводы

  1. Slim CI — паттерн запуска dbt CI на изменённых моделях вместо full build. Технически — state:modified+ --defer --state.
  2. state:modified — селектор, который сравнивает текущий manifest с --state manifest. Включает все модели где изменился checksum (SQL, config, contract, etc.).
  3. DAG операторы: + (downstream), + префикс (upstream), n+ (N уровней). В Slim CI обычно state:modified+ (изменённые + downstream).
  4. state:modified.body / .configs / .macros — гранулярные модификаторы для специальных случаев.
  5. —fail-fast в CI — обязательно. Не плодит ошибки upstream-fail на 50 моделях.
  6. Schema isolation через pr_<NUMBER> schema. Cleanup после PR обязателен (см. урок 1).
  7. Gotchas: изменения в seeds/macros/packages.yml могут быть пропущены Slim CI. Для критичных случаев — fallback на full build.
  8. Экономия compute: типично 80-95% по сравнению с full build. На enterprise команде — десятки тысяч в год.
Проверка знанийKnowledge check
Разработчик добавил generic macro \`{% macro my_helper() %}...{% endmacro %}\` в \`macros/my_helper.sql\`. 30 моделей используют этот macro через {`{{ my_helper() }}`}. После \`dbt build --select state:modified+ --state ./prod/\` он видит что запустилась только модель, которая буквально содержит slang \`my_helper\` в строке — не все 30. Почему и как обойти?
ОтветAnswer
**Причина**: dbt считает модель modified по **checksum raw_code** (текст SQL ДО компиляции). Если macro изменился, но текст модели остался идентичным (`{{ my_helper() }}` ровно та же строка) — checksum модели не меняется. dbt не видит зависимость через manifest в Slim CI по умолчанию.\n\nЭто известная гочча Slim CI: **изменения в macros не транзитивно invalidate models которые их используют**.\n\n**Способы обхода (по сложности):**\n\n**1. state:modified.macros (dbt 1.10+).**\n\n```bash\ndbt build --select state:modified+ state:modified.macros+ --state ./prod/\n```\n\n`state:modified.macros` — селектор, который добавляет в selection все модели которые используют изменённые macros. В 1.10+ это есть нативно.\n\n**2. Custom CI step: detect macro changes -> fallback.**\n\n```yaml\n- name: Check macros changes\n id: macro-check\n run: |\n if git diff origin/main...HEAD --name-only | grep -E '^macros/' > /dev/null; then\n echo "macros-changed=true" >> $GITHUB_OUTPUT\n fi\n\n- name: Full build (macros changed)\n if: steps.macro-check.outputs.macros-changed == 'true'\n run: dbt build --target ci\n\n- name: Slim build (no macros)\n if: steps.macro-check.outputs.macros-changed != 'true'\n run: dbt build --select state:modified+ --defer --state ./prod-state/\n```\n\nЕсли есть изменения в macros/ — full build (надёжно, но дольше). Иначе — Slim CI.\n\n**3. Compiled SQL diff.** Запустить `dbt compile` на feature branch И prod manifest, сравнить compiled SQL построчно. Если compiled отличается -> модель действительно изменилась (через macro). Это сложно, обычно не делают.\n\n**4. Use ref() to macros** (workaround). Можно сделать macro как model — но это анти-паттерн, не делают.\n\n**Рекомендованный путь** — комбинация: `state:modified.macros+` если на dbt 1.10+ (нативно решает), иначе CI fallback на full build при изменениях в `macros/`.\n\n**Главный урок**: Slim CI — оптимизация на 95% случаев. На сложных случаях (macros, seeds, deps) нужен fallback на full build. Это нормально — оптимизация должна быть **корректной**, а не максимально быстрой.
Проверка знанийKnowledge check
Команда настроила \`dbt build --select state:modified+ --defer\`. Junior PR-ит изменение \`fct_orders.sql\`. От \`fct_orders\` зависят 20 моделей. Slim CI запускает все 20 + сам fct_orders = 21 модель. Junior говорит 'это медленно, я же ОДНУ модель изменил'. Что объяснить?
ОтветAnswer
Это **правильное поведение Slim CI**, не баг. Объяснение:\n\n**Почему запускаются 20 downstream:**\n\n`state:modified+` означает 'изменённые + ВСЕ downstream'. Изменение в `fct_orders` может **сломать** любую из 20 зависящих моделей: изменился schema (колонку переименовали), изменилась логика (новые значения в строках), изменился contract. Цель Slim CI — обнаружить **transitive damage**, не только проверить что сам `fct_orders` валиден.\n\nЕсли мы запустим только `fct_orders` без downstream — пропустим возможность сломать что-то downstream, и в prod после merge всё рассыпется. Это **по дизайну**, не optimization fail.\n\n**Альтернативы и trade-offs:**\n\n**1. state:modified без +**\n\n```bash\ndbt build --select state:modified\n```\n\nЗапускает только изменённую (1 модель). Опасно — пропускает downstream damage. **Не делать в Slim CI**.\n\n**2. state:modified+1 (только 1 уровень downstream)**\n\n```bash\ndbt build --select state:modified+1\n```\n\nКомпромисс: запустим immediate dependents, но не глубже. Может пропустить downstream-of-downstream проблем.\n\n**3. state:modified+ + selective execution**\n\nЗапустить только ТЕСТЫ для downstream, не build:\n\n```bash\ndbt build --select state:modified --resource-type model # build только изменённые модели\ndbt test --select state:modified+ --defer --state ./prod/ # test downstream\n```\n\nКомпромисс: downstream **строятся** через defer (как proxy на prod), но **тестируются** против новых данных от `fct_orders`. Быстрее, но сложнее в настройке.\n\n**4. Изменения в `fct_orders` фундаментальны.** Если `fct_orders` — это центральная фактовая таблица в проекте от которой зависит ВСЁ, то факт что 'много моделей зависит' — это не проблема Slim CI, это **высокий blast radius изменения**. Junior должен понимать что изменение центральной модели = большое влияние = много проверок. Это reality check.\n\n**Как объяснить junior:**\n\n'Изменение fct_orders это не "одна модель". Это изменение, которое потенциально влияет на 20 production reports. CI запускает downstream чтобы УБЕДИТЬСЯ что merge не сломает прод. 21 модель за 5 минут — нормально, потому что full build был бы 200 моделей за 30 минут. Slim CI и так оптимизировал в 10 раз. Если бы fct_orders не имела 20 downstream — Slim CI запустил бы 1 модель. Здесь много downstream — отражение того, что fct_orders центральная.'\n\n**Главный урок**: Slim CI — это balance correctness и speed. Запустить меньше — потерять корректность. Slim CI оптимизирует там, где можно (не трогает unchanged), но downstream check критичен.

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

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

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

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