Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 14.04 · 30 мин
Средний
--deferSlim CIstatedeferralupstream resolution

—defer: deep dive в механику

В предыдущем уроке мы запускали dbt build --select state:modified+ --state ./prod/. Но был один кусок неявный: что произойдёт, если изменённая модель зависит от unchanged? Например, новая модель fct_orders использует {'{{ ref(\'stg_customers\') }}'}, но stg_customers не менялась.

Без --defer: dbt будет искать stg_customers в CI schema (pr_123.stg_customers), которая пустая -> ERROR relation does not exist.

С --defer: dbt вместо построения unchanged моделей в CI ссылается на prod schema (prod.stg_customers). Запрос работает.

В этом уроке — детальный разбор как defer работает, что копируется/нет, какие edge cases.


Базовая механика defer

dbt build \
  --select state:modified+ \
  --defer \
  --state ./prod-state/ \
  --target ci
Что делает --defer
Шаг 1: state:modified+ определяет какие модели запускатьstate:modified+ = только изменённые + downstream. Если unchanged модель upstream — она НЕ в selection.
Шаг 2: --defer заставляет ref() unchanged моделей резолвиться в prod--defer говорит dbt: для unchanged зависимостей используй ссылку на prod state. Когда компилируется CI модель, `{{ ref('stg_customers') }}` где stg_customers не в selection -> defer резолвит его в prod.stg_customers.
Шаг 3: build выполняется с гибридными ссылкамиИзменённая модель fct_orders компилируется как `SELECT * FROM prod.stg_customers JOIN ci.pr_123.new_int_model`. Чтение prod таблиц + запись в CI schema. dbt не дублирует unchanged модели в CI.

Visually это похоже на:

-- Без --defer
CREATE TABLE pr_123.fct_orders AS
SELECT * FROM pr_123.stg_customers c
INNER JOIN pr_123.stg_orders o
  ON c.id = o.customer_id
-- ERROR: pr_123.stg_customers не существует

-- С --defer
CREATE TABLE pr_123.fct_orders AS
SELECT * FROM prod.stg_customers c              -- ← deferred
INNER JOIN pr_123.stg_orders o                  -- ← новая модель из CI
  ON c.id = o.customer_id
-- OK: читаем prod, пишем в CI

—state vs —defer-state

В dbt 1.7+ появилось разделение:

ФлагЧто значит
--statemanifest для comparison (определить modified). Без deferral.
--defer-statemanifest для deferral (резолв ссылок в prod). Без comparison.
--state + --deferодин manifest и для comparison, и для defer (классический Slim CI).

В типичном Slim CI используют третий вариант — один и тот же manifest. Но в сложных кейсах можно разделить:

# Чуть продвинутый: сравниваем с одним manifest, defer на другой
dbt build \
  --select state:modified+ \
  --state ./staging-manifest/ \
  --defer-state ./prod-manifest/

Например, staging-manifest — это manifest от integration tests (где stg_customers уже включает новую колонку), а prod-manifest — production (где старая версия). Можно различить modified от prod, но defer на staging.

В большинстве случаев — один manifest. Не усложняйте без необходимости.


Что копируется/не копируется

Defer работает только на чтение. Не копирует данные, не создаёт таблицы. dbt просто переписывает ref()/source() в compiled SQL.

ЧтоДействие при --defer
Unchanged models (ref)Резолвится в prod schema
Sources (source())Резолвится как обычно — в schema, заданную в _sources.yml
SeedsРезолвится в prod schema (если есть в manifest)
SnapshotsРезолвится в prod schema
Test’ыРезолвятся через ту же логику

source() интересен — он не deferred, потому что source — это external таблица (raw данные от Fivetran). И в dev, и в prod source ссылается на одну и ту же raw схему. Defer не нужен.


Ограничения и подводные камни

1. Cross-schema permissions

Если в prod CI пользователь не имеет SELECT на prod schema — defer упадёт:

ERROR: SQL compilation error: schema "PROD" does not exist or not authorized

Решение — grant’ы:

-- В Snowflake
GRANT USAGE ON SCHEMA PROD TO ROLE CI_ROLE;
GRANT SELECT ON ALL TABLES IN SCHEMA PROD TO ROLE CI_ROLE;
GRANT SELECT ON FUTURE TABLES IN SCHEMA PROD TO ROLE CI_ROLE;

Без FUTURE TABLES grant — каждая новая prod таблица не будет видна CI.

2. Incremental модели и unique_key conflicts

{'{{ config(materialized=\'incremental\', unique_key=\'order_id\') }}'}

SELECT * FROM {'{{ ref(\'stg_orders\') }}'}

С --defer defer-резолв даёт prod.stg_orders. Если order_id 123 уже есть в prod.fct_orders (production result), то новый PR build тоже создаст 123 в pr_123.fct_orders. Но это разные таблицы — конфликт не происходит.

Реальный edge case: разработчик меняет unique_key='order_id' на unique_key='order_uuid'. Slim CI запускает fct_orders (изменилась config). Но prod.stg_orders может не иметь колонки order_uuid — она появится только после merge. Defer ссылается на prod stg_orders -> колонка отсутствует -> SQL error.

Решение — fast follow PRs. Сначала smerge изменения в stg_orders (с новой колонкой order_uuid), потом отдельный PR на fct_orders.

3. Custom schema mismatch

Если в проекте используется generate_schema_name с target.name == 'prod'-conditional — defer всегда резолвится в production-схему.

Но если custom_schema задаётся через config:

models:
  jaffle_shop:
    finance:
      +schema: finance        # final schema = analytics_finance в prod

В prod manifest хранится database: analytics, schema: analytics_finance. Defer резолвит туда. Это работает корректно.

Гочча — когда CI target использует другой database. Например, prod в ANALYTICS_DB, CI в CI_DB. Defer всегда смотрит на manifest’s database. Если в prod manifest стоит ANALYTICS_DB.analytics_finance.fct_orders — defer оттуда и возьмёт.

4. dbt версия mismatch

Если prod manifest от dbt 1.10, а CI на dbt 1.11 — структура manifest может различаться. dbt пытается читать legacy формат, но не всегда корректно. Решение — после upgrade dbt в prod, дать пройти полному prod build, потом продолжить Slim CI.


—defer без —select

Можно запустить defer на ВЕСЬ build без selection:

dbt build --defer --state ./prod-state/

Это эквивалентно dbt build (полный run), где unchanged модели резолвятся в prod. Использование:

  • Хотите проверить весь DAG на изменения логики в макросе/конфиге.
  • Тестируете большой refactor где Slim CI ненадёжен.

Но обычно это лишний compute — full Slim CI ловит то же самое.


—defer для конкретного селектора

Можно использовать defer вне Slim CI — для частичных runs:

# Запустить только marts/, читать staging/intermediate из prod
dbt build --select marts.* --defer --state ./prod-state/

Это удобно когда хотите сфокусироваться на конкретном слое:

  • В development: «я работаю над marts, не хочу строить staging заново».
  • В CI: «нужно протестировать marts с свежими данными staging из prod».

defer + tests

Тесты тоже deferred. Когда тест unique пишется в:

models:
  - name: stg_customers
    columns:
      - name: customer_id
        data_tests:
          - unique

Тест компилируется в SQL:

SELECT customer_id, COUNT(*) FROM {'{{ ref(\'stg_customers\') }}'} GROUP BY 1 HAVING COUNT(*) > 1

С --defer ref('stg_customers') для unchanged модели резолвится в prod.stg_customers. Тест проверяет prod данные, не CI build.

Это полезно — мы убеждаемся, что unique уже соблюдается в prod (если PR не изменил эту модель).

Но есть гочча: если ваша CI среда хочет полную проверку (изолированно), это не то поведение. Можно skipать тесты defer’нутых моделей:

dbt test --select state:modified --defer --state ./prod-state/
# Тесты только на изменённые модели, не downstream

Производительность defer

dbt-iii: manifest.json use cases — Slim CI, observability, cost

Defer практически бесплатен на чтение — это просто переписывание ref() в SQL. Но есть compute стоимость:

  • Чтение prod таблиц через JOIN — оплачивается тем, кто платит за CI compute. На Snowflake это CI warehouse читает данные prod таблиц.
  • No data movement — defer не копирует данные между schemas. Это критично для performance.

Сравнение:

Без defer (нужно построить ВСЕ unchanged модели в CI):
- 200 моделей × 10MB средний = 2GB записи в CI schema
- Время: 20 минут
- Стоимость: $5

С defer (читаем prod, пишем только изменённое):
- 13 моделей × 10MB = 130MB записи
- Время: 3 минуты
- Стоимость: $0.5

Дебагинг defer

При проблемах с defer полезно посмотреть compiled SQL:

dbt compile --select fct_orders --defer --state ./prod-state/
cat target/compiled/jaffle_shop/models/marts/fct_orders.sql

Откроется compiled SQL. Если defer работает правильно — увидите prod.schema.stg_customers для unchanged моделей. Если нет — увидите ci.pr_123.stg_customers (defer не сработал).

Частые причины не-работы:

  • Забыли --defer (только --state).
  • Модель в selection (state:modified+ её включает — она будет строиться в CI, не defer).
  • Manifest не имеет нужной модели (старая версия без неё).

Полный workflow с defer

- name: Slim CI build with defer
  run: |
    dbt build \
      --target ci \
      --select state:modified+ \
      --defer \
      --state ./prod-state/ \
      --fail-fast

# Можно ещё параллельно тесты только на изменённые
- name: Tests on modified models
  run: |
    dbt test \
      --target ci \
      --select state:modified \
      --defer \
      --state ./prod-state/

В реальности dbt build включает и тесты и snapshots — обычно одной командой достаточно.


Попробуй сам

В вашем dbt-проекте на DuckDB. DuckDB — это single-file db, defer работает немного по-другому (нет cross-database), но принцип тот же.

  1. Сделайте clean build:
dbt build --target dev
cp target/manifest.json ./prod-state/manifest.json
  1. Создайте новую модель models/marts/mrt_test_defer.sql которая ссылается на существующую:
SELECT customer_id, name, NOW() AS test_time
FROM {'{{ ref(\'stg_customers\') }}'}
  1. Запустите с defer:
dbt parse
dbt build --select state:modified+ --defer --state ./prod-state/ --target dev

Только mrt_test_defer должна построиться.

  1. Посмотрите compiled SQL:
cat target/compiled/<project>/models/marts/mrt_test_defer.sql

Увидите как {'{{ ref(\'stg_customers\') }}'} резолвится — для DuckDB это будет main.stg_customers (prod schema).

  1. Без defer:
dbt build --select state:modified+ --state ./prod-state/ --target dev

Тоже сработает (потому что в DuckDB всё в одной db), но в production warehouse без --defer это бы упало с “relation does not exist”.

Бонус: попробуйте удалить prod-state/manifest.json и запустите. Увидите как dbt падает потому что не может resolve modified.


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

  1. —defer — flag для dbt. Заставляет ref() unchanged моделей резолвиться в --state manifest (prod), а не CI schema.
  2. Защита от “relation does not exist” — без defer Slim CI падает на ref() к unchanged моделям, потому что они не built в pr_<NUMBER> schema.
  3. Defer работает только на чтение. Не копирует таблицы, не дублирует данные. Просто переписывает ref() в compiled SQL.
  4. —state vs —defer-state: --state для comparison (modified), --defer-state для resolution. Обычно используют один manifest для обоих.
  5. Cross-schema permissions — CI user должен иметь SELECT на prod schema. Включая FUTURE TABLES (для новых production таблиц).
  6. Edge cases: incremental с изменением unique_key, custom schemas, dbt version mismatch — могут ломать defer. Решения: fast follow PRs, grant’ы, version sync.
  7. Дебагинг: dbt compile + просмотр compiled SQL покажет правильно ли работает defer.
  8. Производительность: defer экономит compute и storage (нет дублирования данных в CI), но добавляет cross-schema reads (минимальная стоимость).
Проверка знанийKnowledge check
Команда внедрила Slim CI с --defer. PR содержит изменение в \`fct_orders\` (added new column). Slim CI падает с 'column user_email does not exist in stg_users'. Разработчик уверен что добавил колонку в stg_users в этом же PR. Что произошло?
ОтветAnswer
Это классический **race condition в Slim CI**.\n\n**Что произошло пошагово:**\n\n1. Разработчик в одном PR изменил И `stg_users` (добавил user_email) И `fct_orders` (использует user_email).\n2. Slim CI: `state:modified+` включает обе модели. [x]\n3. `stg_users` строится в `pr_123.stg_users` с новой колонкой user_email. [x]\n4. `fct_orders` строится в `pr_123.fct_orders`. Использует `{{ ref('stg_users') }}`.\n5. **С --defer ref резолвится по правилу**: если модель в selection — берём из CI schema. `stg_users` в selection -> резолв в `pr_123.stg_users` (где есть user_email). [x]\n\nЭто **должно работать**. Если падает — другая причина.\n\n**Реальная причина** — скорее всего одна из:\n\n**1. Defer резолв неправильный.** Если по какой-то причине `stg_users` НЕ попал в `state:modified` (например, разработчик сначала закоммитил stg_users в main, а потом начал PR на fct_orders), то stg_users считается **unchanged** -> defer резолвит в `prod.stg_users`, где user_email ещё нет (изменения ещё не в prod).\n\n**2. Ordering execution.** Если `fct_orders` запустился РАНЬШЕ `stg_users` (нарушение DAG). dbt такого не делает, но возможен баг если `ref()` не правильно объявлен.\n\n**3. Build order — `stg_users` упал.** `fct_orders` тогда пытается использовать defer на prod.stg_users (потому что upstream failed in CI). Может быть configured как `--no-fail-fast`. Старая prod версия не имеет user_email -> error.\n\n**Диагностика:**\n\n```bash\n# Проверить что stg_users реально в selection\ndbt ls --select state:modified+ --state ./prod-state/\n\n# Посмотреть compiled SQL для fct_orders\ndbt compile --select fct_orders --defer --state ./prod-state/\ncat target/compiled/.../fct_orders.sql\n```\n\nЕсли в compiled SQL видим `FROM prod.stg_users` (а не `pr_123.stg_users`) — значит defer резолвит на prod. Это race condition. Проверить:\n- Есть ли уже изменения в main? (`git log --oneline -5`)\n- Не оторвалась ли feature branch от main? (`git merge main`)\n\n**Решение проблемы:**\n\n1. **Rebase на свежий main**: если изменения в stg_users уже в main без последнего PR'ового update — manifest от prod уже включает их -> defer работает.\n2. **Fresh prod manifest**: убедиться что prod build прошёл после merge stg_users.\n3. **Combined PR**: оба изменения в одном PR (как сейчас) с правильным DAG — должно работать. Проблема скорее в orchestration или stale manifest.\n\n**Главный урок**: defer + Slim CI имеют edge cases при тесных сочетаниях зависимостей. Если ломается — `dbt compile` показывает что именно резолвилось.
Проверка знанийKnowledge check
В CI у пользователя \`ci_user\` есть SELECT на prod.PUBLIC. Slim CI работает. Через месяц добавили в проект новую модель в схеме \`prod.MARKETING\`, в которую CI пользователь не имеет доступа. Slim CI начинает падать на ВСЕХ PR-ах с 'schema MARKETING does not exist or not authorized'. Что не так и какое долгосрочное решение?
ОтветAnswer
**Что не так**: При запуске Slim CI dbt parse читает manifest и пытается резолвить ref() для всего DAG (включая новую `prod.MARKETING.dim_segments`). Defer пытается прочитать таблицу — нет permissions -> fail.\n\nПо умолчанию CI user имел grant только на старые схемы (PUBLIC). Новые prod schemas создаются по мере роста проекта (по domain), но grants для CI user не обновляются автоматически.\n\n**Краткое решение (workaround):**\n\nДать CI user SELECT на новую схему:\n\n```sql\nGRANT USAGE ON SCHEMA PROD.MARKETING TO ROLE CI_ROLE;\nGRANT SELECT ON ALL TABLES IN SCHEMA PROD.MARKETING TO ROLE CI_ROLE;\nGRANT SELECT ON ALL VIEWS IN SCHEMA PROD.MARKETING TO ROLE CI_ROLE;\n```\n\nЭто решает проблему до следующей новой схемы. Не масштабируется.\n\n**Долгосрочное решение — FUTURE TABLES grant:**\n\n```sql\n-- Snowflake — гранты для будущих объектов\nGRANT USAGE ON FUTURE SCHEMAS IN DATABASE PROD TO ROLE CI_ROLE;\nGRANT SELECT ON FUTURE TABLES IN DATABASE PROD TO ROLE CI_ROLE;\nGRANT SELECT ON FUTURE VIEWS IN DATABASE PROD TO ROLE CI_ROLE;\n```\n\nС FUTURE TABLES — каждая новая таблица/view, которую создаст dbt в prod, автоматически получает grant CI_ROLE. CI всегда видит весь prod.\n\nДля BigQuery/Postgres — другой механизм:\n- **Postgres**: `ALTER DEFAULT PRIVILEGES IN SCHEMA prod GRANT SELECT ON TABLES TO ci_role`. Только для objects созданных user'ом который выполнил ALTER.\n- **BigQuery**: dataset-level access (`bq update --add_user user:ci@... --role READER my-project:prod`) — все будущие tables в этом dataset получают grant.\n\n**Альтернативное решение — grants через dbt:**\n\n```yaml\n# dbt_project.yml\nmodels:\n jaffle_shop:\n +grants:\n select: ['ci_role', 'analyst_role']\n```\n\nДля каждой новой prod модели dbt автоматически выполнит GRANT SELECT на CI_ROLE и ANALYST_ROLE после build. Это **declarative** подход — описано в коде, не в admin-настройках warehouse.\n\n**Главный урок**: при настройке Slim CI с --defer нужно думать о permissions для **будущих** объектов, не только текущих. FUTURE TABLES grant (Snowflake) или dbt-managed grants — это must-have паттерн. Иначе через месяц у вас ломается CI каждый раз когда команда добавляет новую схему.

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

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

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

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