Troubleshooting — dbt II
База знаний типичных ошибок курса dbt II.
Причина
prod manifest (--state ./prod-state) не обновлён — модели сравниваются со старым состоянием
Решение
- После каждого успешного prod deploy сохраняйте свежий manifest.json в storage (S3/gh-pages/CI artifacts) с тэгом latest. В CI workflow первым шагом скачивайте этот manifest перед
dbt build --select state:modified+ --defer --state ./prod-state. Используйте версионирование (commit-sha-tagged), чтобы откатывать на конкретный manifest при инцидентах.
Причина
incremental_strategy='delete+insert' с unique_key, который не уникален в incoming батче или в target таблице, выполняет DELETE FROM target JOIN incoming ON unique_key — каждое совпадение удаляется N раз
Решение
- Либо реально гарантируйте уникальность через staging-модель с QUALIFY ROW_NUMBER() = 1, либо используйте составной unique_key (массив), либо переходите на merge-стратегию с merge_update_columns. Test
unique_combination_of_columnsиз dbt-utils в_models.ymlловит это на CI до prod-инцидента.
Причина
MERGE без incremental_predicates сканирует ВСЮ target-таблицу для поиска matching, и без merge_update_columns обновляет все колонки даже если значения те же
Решение
- Добавьте
incremental_predicates: ['DBT_INTERNAL_DEST.event_date >= dateadd(day, -7, current_date)']— критично использоватьDBT_INTERNAL_DEST.префикс, иначе MERGE silent делает full scan. Дополнительноmerge_update_columns: ['amount', 'status']чтобы не writать неизменяемые колонки.
Причина
Забыли --full-refresh путь: вся логика модели обёрнута в `{% if is_incremental() %}`, и на первом запуске модель возвращает 0 строк
Решение
- Структура должна быть: основной SELECT с агрегациями всегда выполняется, а
{% if is_incremental() %} WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }}) {% endif %}только добавляет фильтр. На первом run is_incremental=False, фильтр отсутствует, модель строит полный набор.
Причина
microbatch требует, чтобы event_time был в UTC; column с local-time или TIMESTAMP WITHOUT TIME ZONE вызывает ошибку парсинга батчей
Решение
- Каст source-колонки в UTC явно в staging-модели:
TIMESTAMP WITH TIME ZONE 'UTC' AT TIME ZONE source_tzилиevent_at::timestamp at time zone 'UTC'. В yml пометьте event_time как UTC через комментарий. Используйте{{ var('event_time_zone', 'UTC') }}для тестируемости.
Причина
По умолчанию microbatch разрешает full-refresh, и accidental запуск стирает всю историю с заметным billable warehouse usage
Решение
- Защитите production-модели через
config(materialized='incremental', incremental_strategy='microbatch', full_refresh=false). Если действительно нужен rebuild, временно снимите конфиг или используйте--full-refresh --select fct_ordersявно. Для backfill —--event-time-start/--event-time-endбез --full-refresh.
Причина
dbt по умолчанию ставит end=now(), что в проде = огромный диапазон -> миллион батчей и многочасовой run
Решение
- Всегда указывайте оба флага:
dbt run --select fct_events --event-time-start 2024-01-01 --event-time-end 2024-02-01. Запускайте backfill по неделе / месяцу за раз. Мониторьте concurrent_batches на warehouse-pressure.
Причина
Default lookback=1 — пересматривается только один прошлый батч. Если событие приходит с задержкой 2+ дней, оно остаётся вне диапазона
Решение
- Увеличьте
lookback: 7(или больше, по реальным observed delays). Альтернатива: периодический backfill через --event-time-start. Метрика для мониторинга:% rows where insertion_timestamp - event_time > batch_size.
Причина
snapshot unique_key не уникален в source — каждый run создаёт по новой строке для каждого дубликата
Решение
- Прежде чем дать unique_key в snapshot, валидируйте source: добавьте generic test
uniqueилиunique_combination_of_columns. Если естественной уникальности нет — используйте composite key или surrogate (md5 от строки). Snapshot нельзя 'починить retroactive' без потери истории.
Причина
После добавления колонки в check_cols snapshot считает ВСЕ existing rows changed (новая колонка vs missing -> diff)
Решение
- Используйте timestamp strategy с надёжной
updated_at— она устойчивее к schema changes. Если check strategy обязательна, при добавлении колонки сделайте explicitdbt snapshot --select my_snapshot --vars '{check_cols_changed: true}'(custom flag) ИЛИ временно удалите колонку из check_cols пока история не нагонит.
Причина
Дефолт `hard_deletes: ignore` — если строка исчезает из source, snapshot её не закрывает (dbt_valid_to=NULL навсегда)
Решение
- Включите
hard_deletes: invalidateв snapshot config — при отсутствии строки в source dbt закроет dbt_valid_to=now(). Для аудиторного следа:hard_deletes: new_recordдобавляет marker-строку сdbt_is_deleted=true. DuckDB: hard_deletes пока не поддерживается, нужна Postgres/Snowflake.
Причина
Запросы вида `WHERE date_col BETWEEN dbt_valid_from AND dbt_valid_to` возвращают пустоту для текущей версии (NULL > date -> False)
Решение
- Включите
dbt_valid_to_current: '9999-12-31'в snapshot config — для открытых версий dbt подставит этот sentinel. Все BI-запросы становятсяdate_col BETWEEN dbt_valid_from AND dbt_valid_toбез NULL-обработки. Доступно с dbt-core 1.9+.
Причина
Команда оставила severity на default или скопировала warn — failures не блокируют merge
Решение
- Audit
_models.yml: для бизнес-критичных тестов (uniqueness PK, FK integrity, not_null) — обязательно severity=error. Для soft-проверок (cardinality, range) —severity: warn+error_if: '>X'. Запустите grep для аудита:grep -r 'severity: warn' models/.
Причина
`store_failures: true` создаёт таблицы в `<project>_dbt_test__audit.<test_name>` без cleanup; на больших проектах — сотни таблиц с failure-данными
Решение
- В on-run-end установите cleanup:
DROP TABLE IF EXISTS <project>_dbt_test__audit.<table> CASCADEдля тестов старше N дней. Альтернатива:store_failures_as: view(1.9+) — view не хранит данные, lazy-eval. Для прод-инцидентов сохраняйте failures как partitioned table.
Причина
Дефолтный relationships тест делает LEFT JOIN ON foreign_key и считает orphans — на >10M rows это full scan + сортировка
Решение
- Замените на
dbt_utils.relationships_whereс filter на recent partition:where: 'created_at >= dateadd(day, -7, current_date)'. Альтернатива: severity=warn + custom тест с EXISTS subquery. Регулярно: добавьте index/partition на FK-колонку в warehouse.
Причина
Запустили `--select state:modified` без +, изменённая модель собралась, но её downstream не пересобрались. Merge в main вызвал break-разрыв
Решение
- Всегда используйте
state:modified+в Slim CI. Плюс расширяет селекцию на ВСЕ downstream-зависимости. Для cross-project —state:modified+,+state:modified(вверх и вниз). Тест: запустите CI, проверьте, что в output есть не только сами модели, но и их fct_/dim_ потребители.
Причина
На CI с --defer ref на не-modified модели резолвится в prod-схему. Если prod-данные мутируют между двумя CI runs, тесты могут пройти/упасть по-разному
Решение
- Защитите prod-схемы от мутаций во время CI: либо clone схемы (Snowflake zero-copy) в CI startup, либо снимок prod-данных в test fixtures. Альтернатива: --defer только для models, тесты — на CI-collected data. Документируйте flaky-tests в run_results, отслеживайте patterns.
Причина
`dbt source freshness` не входит в production job, daily fail остаётся незамеченным — модели строятся на устаревшем источнике
Решение
- В prod-deploy workflow первым шагом:
dbt source freshness || (notify Slack + exit 1). Используйте thresholderror_after(не warn_after) в_sources.ymlдля бизнес-критичных. Для CI достаточно warn_after — не блокирует merge. Для prod — error_after обязательно.
Причина
Команда установила `pre-commit-config.yml` но забыла `pre-commit install` — хуки прописаны декларативно, но не зарегистрированы в .git/hooks
Решение
- После клона репо каждый разработчик ОБЯЗАН запустить
pre-commit install. Документируйте в README + добавьтеpre-commit installв make-target (make bootstrap). Альтернатива: централизованный setup через scripts/install-hooks.sh, который проверяет version и предупреждает.
Причина
sqlfluff без `dbt` templater читает `{{ ref('foo') }}` как SQL-выражение, выдаёт linting errors
Решение
- В
.sqlfluffобязательноtemplater = dbt+[sqlfluff:templater:dbt] project_dir = .. Проверьте, чтоdbt-coreдоступен в env. Для CI: запускайте sqlfluff черезsqlfluff lint --templater=dbt models/. Альтернативно: использовать sqlfluff-templater-dbt отдельный пакет.
Причина
В CI workflow не передан `--target ci` или `--target prod`, dbt берёт default из profiles.yml (обычно dev) -> CI пишет в dev schema
Решение
- Всегда явно:
dbt build --target ci --profiles-dir ./profiles/. В profiles.yml для CI отдельный target с уникальным schema (schema: dbt_ci_pr{{ env_var('GITHUB_PR_NUMBER') }}). Post-CI hook:DROP SCHEMA dbt_ci_pr{number} CASCADEпосле merge.
Причина
`{{ env_var('SOMETHING') }}` без второго аргумента: если SOMETHING не задано, parse fail с 'Env var required but not provided'
Решение
- Для всех optional env_var всегда давайте default:
{{ env_var('FEATURE_FLAG', 'false') }}. Для required — оставьте без default и зафиксируйте в README/CI required env_vars. Хорошая практика: в начале dbt_project.yml вызватьenv_var('CRITICAL_VAR')для fail-fast.
Причина
В dbt_project.yml забыли указать `dispatch: - macro_namespace: dbt_utils, search_order: ['my_project', 'dbt_utils']`. dbt берёт первую попавшуюся реализацию (часто из dbt-utils)
Решение
- Явно укажите search_order в dbt_project.yml. Для каждого macro_namespace (dbt, dbt_utils): сначала ваш проект, потом packages. Тестируйте через
dbt run-operation my_macro— если выполнилась версия из package, search_order настроен неверно.
Причина
Override без `if target.name == 'prod'` — даже на prod target dbt именует схемы с суффиксом (analytics_marts вместо marts)
Решение
- Канонический override:
{% if target.name == 'prod' %}{{ custom_schema_name | trim }}{% else %}{{ target.schema }}_{{ custom_schema_name | trim }}{% endif %}. На prod кастомное имя как-есть, на остальных — суффикс. Тестируйте:dbt compile --target prod-> проверьте target/compiled/ paths.
Причина
Comment длиннее warehouse-limit (Snowflake 16K, BigQuery 1K), warehouse silent обрезает или fail — но dbt продолжает run
Решение
- Аудит yml-описаний: для длинных используйте
{% docs %}блоки +doc()reference (короткий handle в COMMENT). Для warehouse без поддержки column-comments отключите:persist_docs: {relation: true, columns: false}.
Причина
На warehouses без real constraint enforcement (BigQuery, Snowflake до 2023) constraints записываются как metadata-only — данные могут нарушать констрейнт, dbt этого не заметит
Решение
- Добавьте explicit data tests на критичные constraints:
unique,not_nullв_models.yml. Contract enforced проверяет только columns + types, не data integrity. Для PK uniqueness —dbt_utils.unique_combination_of_columns. CI должен запускать оба слоя.
Причина
В yml model versions нет `latest_version: 1` -> `ref('model')` без v= резолвится в максимальную доступную версию. Когда выкатили v2 (несовместимую), все downstream сломались
Решение
- Всегда явно
latest_versionв yml. Когда выкатывается v2: пишите её сdefined_in: model_v2.sql, оставляйте latest_version=1, postupenno мигрируйте downstream наref('model', v=2). После миграции переключите latest_version=2.
Причина
`grants: {select: ['analyst_role']}` без `+` (cumulative) на каждом run перезаписывает grants. Любой ручной GRANT, добавленный вне dbt, теряется
Решение
- Для аддитивных prod-сценариев:
grants: {+select: ['bi_role', 'analyst_role']}. Плюс делает GRANT cumulative — старые grants сохраняются. Для строгого декларативного управления: оставьте без+, но запретите ручные GRANTs (RBAC через Terraform/Snowflake DDL).
Причина
В моделях прописано `WHERE env = 'prod'` вместо `WHERE env = '{{ target.name }}'` — на dev запросы возвращают пусто
Решение
- Используйте
{{ target.name }}или{{ var('environment', target.name) }}для маппинга. Для тестов: dbt unit tests сoverrides: {target.name: 'prod'}. Аудит:grep -rn "'prod'\|'dev'" models/ловит hard-coded.
Причина
На dev `dbt snapshot --full-refresh` или dropping/recreating snapshot schema стирает историю — теряется SCD2 state
Решение
- Snapshots должны жить в durable schema даже на dev. В profiles.yml: snapshots target schema =
dbt_snapshots_dev(стабильная). Никогда--full-refreshна snapshots. Если нужно начать заново — explicit DROP конкретного snapshot, не reset всей dev схемы.
Причина
Default `materialized=view` забыт; через копипасту/codegen все модели table. Warehouse cost растёт линейно от количества моделей
Решение
- Аудит:
dbt list --resource-type model --output json | jq '.[] | select(.config.materialized=="table") | .name'. Для intermediate —ephemeral(если простые) илиview. Толькоfct_/dim_финальные — table/incremental. Большие — incremental с partition.
Причина
Регулярный prod-job запускает `dbt test` целиком — тысячи тестов на не-изменившихся моделях, warehouse cost растёт
Решение
- Разделите тесты: critical (uniqueness, not_null) — каждый run после build, остальное — daily/weekly job с
--select tag:slow. Используйтеwhere:filter в тестах для ограничения scope (last 7 days). Для freshness — отдельный job (5-min cron).
Причина
На Snowflake Iceberg table dbt 1.11 поддерживает только view + table, не incremental. Build падает с 'Iceberg unsupported for incremental'
Решение
- Wait for 1.12+ official support OR использовать workaround: external table + post-hook INSERT INTO. Альтернатива — Native Snowflake table (не Iceberg) для incremental, periodic snapshot в Iceberg через на separate job. Trackайте dbt-snowflake issues.
Причина
В microbatch включили concurrent_batches=true, но модель не идемпотентна (например, ROW_NUMBER() OVER без partition by event_time) — race condition даёт inconsistent state
Решение
- Включайте concurrent_batches=true только для агрегаций без order-dependency. Для SCD/cumulative/window-functions — оставьте
concurrent_batches: false. Проверка: запустите N раз с concurrent=true vs concurrent=false -> результаты должны совпадать.
Причина
`{% docs my_block %}` определён в `models/docs.md`, но `dbt docs generate` не запускался — references к doc('my_block') в yml fail с 'doc not found'
Решение
- Включите
dbt docs generateкак первый шаг CI (или в pre-commit). Без него parse fail на doc(). Альтернативно: явный testdbt parseв CI — он валидирует doc references на parse-time без полного docs build.
Причина
Обновили `packages.yml`, но не запустили `dbt deps` — модели берут старую версию package из `dbt_packages/`, новые макросы не доступны
Решение
- В CI/dev обязательно:
dbt depsперед каждымdbt run. В pre-commit hook: чекать, что packages.yml не изменился без обновления package-lock.yml. В Makefile:make buildзависит отmake deps.
Причина
`incremental_strategy='merge'` требует unique_key (для ON-условия). Без него dbt fail на parse phase с 'merge requires unique_key'
Решение
- Добавьте
unique_key='order_id'(single column) илиunique_key=['order_id', 'item_id'](composite). Если естественной уникальности нет — surrogate key черезdbt_utils.generate_surrogate_key(['col1', 'col2'])в модели.
Причина
microbatch стратегия by-design без unique_key — она работает через DELETE+INSERT per батч. unique_key игнорируется и вызывает config validation error
Решение
- Уберите unique_key из microbatch config. Гарантию идемпотентности обеспечивайте через event_time + batch_size (DELETE по диапазону событий). Если внутри батча возможны дубли — фильтруйте на staging (QUALIFY ROW_NUMBER()).
Причина
Из project_b сделали `ref('project_a', 'model')`, но в project_a `access: protected` — Mesh boundary не позволяет
Решение
- В upstream project_a explicit пометьте модель
access: publicв yml. Согласуйте через group + access — каноничный паттерн: модели в group=public-api с public access, остальные protected. Документируйте public API в exposures.
Причина
В semantic_models забыли primary entity (или поставили только foreign). MetricFlow не может построить JOIN-граф для metric, выдаёт 'no path between entities'
Решение
- Каждая semantic_model должна иметь ровно один
type: primaryentity. Foreign entities ссылаются на primary в других models. Запуститеmf validate-configs— он проверяет, что все entity references resolvable.
Причина
В ratio metric numerator и denominator на разных semantic_models с разной cardinality — MetricFlow делает Cartesian-style JOIN
Решение
- Убедитесь, что numerator и denominator имеют общую entity, через которую корректно агрегируются. Для AOV: revenue (sum amount FROM orders) / order_count (count FROM orders) — оба per-order, JOIN на customer_id (или order_id) гарантирован. Тестируйте через
mf query --explain.
Причина
`window: 7 days` не учитывает дни без событий — нулевые дни выпадают, running sum 'прыгает'
Решение
- Добавьте date_spine seed/model (последовательность дат) и LEFT JOIN к нему. Альтернатива в MetricFlow:
grain_to_dateрежим с явной нулевой защитой. Тестируйте на спарс-данных (период без событий).
Причина
В CLI запустили `mf query --saved-query my_query` (только вывод), но не `mf export` (материализация в warehouse)
Решение
- Для материализации:
mf export --saved-query my_query. Можно поместить в schedule через GitHub Actions или dbt Cloud job. Export пишет в схему как table (default) или view, тип контролируется конфигом saved_query.
Причина
actionlint строго парсит expression-syntax, иногда false positives на dynamic expressions с `${{ secrets.FOO }}` внутри shell-команд
Решение
- Используйте
# actionlint-shellcheckexclude комменты для known-false-positives. Для shell-команд: вынесите в отдельный.shфайл, в workflow вызывайте через./script.sh. Tests:actionlint -ignore '...'.
Причина
В CI workflow `--state` указан на gh-pages URL, но `curl` fail из-за CORS / private repo / неверного пути
Решение
- Для public repo:
curl -sf https://owner.github.io/repo/manifest.json -o prod-state/manifest.json. Для private — используйте GitHub artifact download черезgh run downloadили раздельный bucket S3 c IAM. Тестируйте манифест на каждом deploy.
Причина
Хук `check-script-has-no-table-name` фейлит, потому что модель ещё не в manifest (новый файл, не было dbt parse)
Решение
- Добавьте в pre-commit-config:
args: [--manifest-path, target/manifest.json]и вmake pre-commitзапускайте сначалаdbt parse. Альтернатива: skip hook for new files через# noqa: dbt-checkpoint.
Причина
Unit test `given:` fixture ссылается на input model, но input model изменилась (новые колонки) — unit test становится несовместимым
Решение
- Каждый раз при изменении output input-модели — обновляйте все unit test fixtures, которые её используют. Используйте
dbt parseлокально для catch early. CI:dbt test --select test_type:unitобязательно перед merge.
Причина
`incremental_strategy='merge'` падает с 'MERGE not supported' на DuckDB <1.4. dbt-duckdb 1.10 + DuckDB 1.1 — без MERGE
Решение
- Обновите DuckDB до 1.4+ (через
pip install duckdb>=1.4) ИЛИ замените наdelete+insert(с unique_key для гарантии уникальности). Для production: всегда фиксируйте конкретную версию DuckDB в requirements.txt.
Причина
CI cache `~/.dbt` копит установленные packages, runs артефакты между job runs — занимает гигабайты
Решение
- Кэшируйте только
dbt_packages/(послеdbt deps) и target manifest. Cleanup:find ~/.dbt -name 'logs/*' -mtime +1 -delete. В GitHub Actions:actions/cache@v4с явным pathdbt_packages/+key: dbt-${{ hashFiles('packages.yml') }}.