Learning Platform
Глоссарий Troubleshooting
Урок 07.02 · 25 мин
Средний
dbt-expectationsPackagesDependenciesProduction rollout

dbt-expectations: пакет в проекте

dbt-expectations — порт Python-библиотеки Great Expectations в dbt, 60+ декларативных тестов поверх dbt_utils. Цель пакета — закрыть пробел между «базовыми» тестами (unique, not_null, accepted_values) и полноценной валидацией schema / distribution / statistical properties без написания custom generic-тестов на каждую проверку.

Этот урок — про сам пакет как зависимость проекта: как ставить, чем он тянет за собой dbt_utils, как выбирать между ним и dbt_utils, как версионировать и сколько он реально стоит в dbt test. Каталог конкретных тестов с примерами — в уроке 7.5 «Тур по dbt-expectations», там 15 наиболее ценных макросов с примерами кода.

Data Quality: зачем нужна декларативная валидация

Установка и зависимости

Стандартный package install через packages.yml в корне проекта:

# packages.yml
packages:
  - package: calogica/dbt_expectations
    version: [">=0.10.0", "<1.0.0"]

После этого:

dbt deps

dbt deps читает packages.yml, скачивает указанные версии в dbt_packages/ и резолвит транзитивные зависимости. На выходе вы увидите примерно:

Installing calogica/dbt_expectations
  Installed from version 0.10.4
Installing dbt-labs/dbt_utils
  Installed from version 1.3.0
  Updated version available: 1.4.1

Заметьте вторую строку: dbt-expectations транзитивно ставит dbt_utils. Это потому что многие его макросы (например, expect_compound_columns_to_be_unique) внутри переиспользуют dbt_utils.unique_combination_of_columns или dbt_utils.get_column_values.

Транзитивная зависимость на dbt_utils

Это первая packaging-граната, на которой подрываются проекты, у которых уже стоит dbt_utils явно.

Resolution транзитивной зависимости
packages.yml: dbt_utils 1.2.0Ваш packages.yml явно требует dbt_utils определённой версии (например, 1.2.0). Часто закрепляют, потому что в проекте используется конкретный macro.
dbt_expectations 0.10.4 -> dbt_utils >= 1.3.0dbt-expectations 0.10.4 в своём dependencies.yml требует dbt_utils от 1.3.0 и выше. Версии несовместимы — dbt deps fail.
overrides: dbt_utils ~1.3Решение через секцию overrides в packages.yml — явно фиксируем версию, перекрывая транзитивные требования. Версия должна быть совместима с обоими пакетами.
dbt deps OKdbt deps берёт overrides как источник истины, игнорируя транзитивные ограничения. На свою же ответственность — если override несовместим, упадёт уже dbt parse или dbt run.

На практике:

# packages.yml
packages:
  - package: dbt-labs/dbt_utils
    version: 1.3.0
  - package: calogica/dbt_expectations
    version: 0.10.4

Если у dbt_expectations requirement на dbt_utils >= 1.3.0, конфликта нет. Если у вас стоял dbt_utils 1.2.0 — потребуется upgrade. Обычно проще обновить dbt_utils, чем downgrade-ить dbt_expectations.

В сложных случаях (transitive граф из 3-4 пакетов) появляется секция overrides в packages.yml:

packages:
  - package: dbt-labs/dbt_utils
    version: 1.3.0
  - package: calogica/dbt_expectations
    version: 0.10.4
  - package: dbt-labs/audit_helper
    version: 0.12.0

overrides:
  - package: dbt-labs/dbt_utils
    version: 1.3.0  # фиксируем явно, перекрывая требования транзитивных

overrides говорит dbt: «верь мне, ставлю эту версию для всех, кто её требует». Если override несовместим с реальным API одного из пакетов, dbt parse упадёт уже на загрузке макросов.

Структура dbt_packages/

После dbt deps появляется директория:

dbt_packages/
  dbt_utils/
    macros/
    dbt_project.yml
  dbt_expectations/
    macros/
    dbt_project.yml

dbt_packages/ всегда в .gitignore — это derived directory, как node_modules. Источник истины — packages.yml (что просим) и package-lock.yml (что реально стоит — генерируется dbt deps).

package-lock.yml коммитят: он фиксирует точные версии и SHA. После git pull свежий клон делает dbt deps, читает lock-файл и ставит ровно те же версии, что у автора PR. Без lock-файла на CI вы можете получить другую minor-версию dbt_utils, и тесты внезапно сломаются.


dbt_utils vs dbt-expectations: decision matrix

Два пакета пересекаются — оба тестируют данные, оба предоставляют helpers. На уровне middle важно понимать, для чего каждый предназначен.

Зона ответственности пакетов
dbt_utilsdbt_utils — utility-пакет. Основная ценность — макросы для написания моделей (deduplicate, surrogate_key, union_relations, pivot, date_spine). Тесты есть, но их немного и они вспомогательные.
dbt-expectationsdbt-expectations — testing-пакет. 60+ макросов исключительно для валидации данных: schema, value ranges, distribution, statistical metrics. Helpers для SQL почти нет.

Сценарии:

Нужны оба. Типично для зрелого проекта с marts и dimension-таблицами. dbt_utils.deduplicate в моделях + dbt_expectations.expect_column_mean_to_be_between в тестах. Зон пересечения избегают конвенциями (см. ниже).

Только dbt_utils. Стартовый проект, где нужны helpers для SQL, а тесты пока в пределах dbt-core. Не тащить dbt-expectations ради двух тестов — экономия зависимостей и времени dbt deps.

Только dbt-expectations. Редкий кейс. Если все SQL-помощники уже custom-написаны в macros/, а от пакета нужны только декларативные тесты. На практике почти всегда dbt_utils всё равно нужен — хотя бы как транзитивная зависимость.

Custom generic вместо пакета. Если в проекте нужен один-два тестa специфичных для домена (expect_revenue_to_match_invoice_total), пишите tests/generic/ без пакета. Установка пакета ради 2 макросов — overkill: 60+ макросов в namespace, дополнительная зависимость, обновления.

Зона пересечения: где они дублируются

Несколько макросов делают одно и то же:

Что нужноdbt-coredbt_utilsdbt-expectations
Уникальная пара колонокunique_combination_of_columnsexpect_compound_columns_to_be_unique
Равенство row countequal_rowcountexpect_table_row_count_to_equal_other_table
Условие на строкиexpression_is_trueexpect_row_values_to_have_data_for_every_n_datepart
not_nullnot_nullexpect_column_values_to_not_be_null
accepted_valuesaccepted_valuesexpect_column_values_to_be_in_set

Правило команды: выбрать один источник для каждой группы и зафиксировать в style guide. Например:

  • Базовые unique / not_null / accepted_values — всегда из dbt-core (короче, читается лучше).
  • Composite uniqueness — из dbt_utils (unique_combination_of_columns).
  • Statistical (mean, stddev, quantile) — из dbt-expectations.
  • Schema (column count, types) — из dbt-expectations для проектов pre-1.5, иначе native model contracts.

Без такой конвенции в проекте через год вы найдёте оба варианта в разных моделях, сделанных разными людьми. Это не сломает CI, но добавит шума на code review.


Версионирование и upgrade workflow

dbt-expectations следует semver, но минорные релизы часто содержат breaking changes в сигнатурах макросов — это известная особенность пакета. Не привязывайтесь к major-only ограничениям.

Pinning стратегия

Минимум — указывать совместимый range:

packages:
  - package: calogica/dbt_expectations
    version: [">=0.10.0", "<0.11.0"]

Это разрешит только patch-релизы внутри 0.10.x. Для production проектов это рекомендованная стратегия — patches содержат только bugfix.

Maximum lock — фиксированная версия:

packages:
  - package: calogica/dbt_expectations
    version: 0.10.4

Подходит для регулируемых отраслей, где требуется reproducibility сборки. Но требует ручного upgrade workflow.

Workflow после git pull

После того как кто-то в команде обновил packages.yml или package-lock.yml:

git pull
dbt deps  # обязательно после каждого pull, изменившего packages.yml
dbt parse  # быстрая проверка что макросы резолвятся
dbt test --select package:dbt_expectations  # прогнать только тесты этого пакета

Если dbt deps молчит, но dbt parse падает с 'macro_name' is undefined — значит, в новой версии переименовали или удалили макрос. Проверяйте CHANGELOG.md в репозитории пакета.

Workflow при breaking change

dbt-expectations иногда меняет API. Типичный пример: в 0.9.0 параметр назывался column_name, в 0.10.0 стал позиционным первым аргументом. Все YAML с этим макросом нужно переписать.

Алгоритм:

  1. Не обновляйте версию вслепую. Перед апгрейдом откройте https://github.com/calogica/dbt-expectations/blob/main/CHANGELOG.md и прочитайте секцию для целевой версии.
  2. Создайте feature branch. Изменения версии — отдельный PR, не смешивайте с feature-работой.
  3. Pin новую версию точечно. version: 0.10.4 явно, без range.
  4. Запустите dbt parse --select state:modified+. Это покажет, какие модели/тесты используют изменившиеся макросы.
  5. Обновляйте YAML итеративно. Запускайте dbt parse после каждой партии изменений.
  6. Прогон dbt test. Семантика теста может поменяться — например, expect_column_values_to_be_between в новой версии включает границы, а в старой исключала.
  7. Merge. После зелёного CI и code review.

Обновлять dbt-expectations редко — не чаще раза в квартал. Между апгрейдами добавляйте только новые тесты на текущей версии, не лезьте в старые ради «новой фичи».


Cost: каждый expect_* — это SQL

Декларативность пакета скрывает простую правду: каждый макрос компилируется в отдельный SELECT с агрегатами. На крупном проекте это серьёзная статья в стоимости dbt test.

Cost dbt-expectations на типичном проекте
200 моделей × 4 тестаТипичный проект middle-уровня: 200 моделей, на каждой 3-5 тестов из dbt-expectations. Это 800 SQL-запросов на dbt test.
800 SQL800 запусков SELECT с агрегатами (COUNT, AVG, STDDEV, percentile). На DuckDB локально ~5 мин. На Snowflake medium warehouse ~8-10 мин.
+5-10 мин к pipelineПрибавка к dbt test pipeline. На локальной разработке терпимо, на CI и каждом push — заметно. Optimizations: severity filter, parallelism, partial testing.

Контрольные цифры (грубо, DuckDB на mid-tier ноуте):

  • expect_column_values_to_not_be_null на 1M rows: ~50ms.
  • expect_column_values_to_be_between на 1M rows: ~80ms (TABLE SCAN + WHERE).
  • expect_column_mean_to_be_between на 1M rows: ~150ms (агрегат).
  • expect_column_quantile_values_to_be_between на 1M rows: ~300ms (сортировка для perc).
  • expect_column_distinct_count_to_be_between на 10M rows: ~1.5s (distinct).

Помножьте на количество моделей и тестов — получите бюджет на dbt test.

Стратегии оптимизации

1. severity и где фильтры. Не все тесты нужно гонять на каждый run.

- dbt_expectations.expect_column_mean_to_be_between:
    min_value: 100
    max_value: 200
    config:
      severity: warn
      where: "order_date >= current_date - 7"

where снижает scope с «вся таблица» до «последняя неделя». Для drift-тестов разница в 100x.

2. tag и selectors. Разделите тесты на «всегда» и «nightly»:

- dbt_expectations.expect_column_distinct_count_to_be_between:
    min_value: 50
    max_value: 250
    config:
      tags: ['nightly_only']

В CI: dbt test --exclude tag:nightly_only. В nightly job: dbt test --select tag:nightly_only.

3. store_failures. По умолчанию падающий тест пишет в dbt.log сводку. Если включить store_failures: true, dbt создаёт таблицу с failing rows. Удобно для investigation, но добавляет CTAS на каждый тест — это +20-40% к runtime. Для CI обычно выключают, для local debug включают.

4. Parallelism. dbt test --threads 8 запускает несколько тестов параллельно. На DuckDB это уменьшит wall time, но не CPU time. Тестируйте — иногда threads > 4 на DuckDB вызывает contention.

Cost — это не аргумент против dbt-expectations, это аргумент за калибровку. Если 800 тестов добавляют 10 минут — это нормально, пока эти тесты ловят реальные баги. Если из 800 ни один не упал за квартал — пора почистить.


Production rollout pattern

Опасный антипаттерн: на новой неделе добавить 50 тестов из dbt-expectations сразу на все модели. CI краснеет, команда привыкает игнорировать алерты, через месяц никто на них не смотрит.

Постепенный rollout по 4 этапам, на пилот-модели:

Этап 1: pilot на одной важной модели (1 неделя)

Выберите модель с наибольшим business impact — обычно центральная mart (fct_orders, dim_customers). Добавьте 3-5 тестов: row_count, schema, business rules. Severity warn.

# models/marts/fct_orders.yml
- name: fct_orders
  data_tests:
    - dbt_expectations.expect_table_row_count_to_be_between:
        min_value: 100
        max_value: 100000
        config:
          severity: warn

Цель — увидеть, сколько false positives, сколько реальных issues, как команда реагирует на warnings.

Этап 2: расширение на критичный слой (2 недели)

Добавьте тесты на все mart-модели. Сохраняйте severity warn для статистических, error для structural (schema, PK uniqueness).

Зафиксируйте naming convention в style guide:

# Стиль команды: префикс тегом для классификации
- dbt_expectations.expect_column_mean_to_be_between:
    min_value: 100
    max_value: 200
    config:
      severity: warn
      tags: ['dq:statistical']
- dbt_expectations.expect_compound_columns_to_be_unique:
    column_list: [order_id, line_item_id]
    config:
      severity: error
      tags: ['dq:integrity']

Теги позволяют делать dbt test --select tag:dq:integrity для smoke-проверки в PR.

Этап 3: эскалация severity (после стабилизации, ~2 недели)

Через 2 недели с момента добавления, если статистический тест ни разу не упал ложно — повысьте severity до error. Команда уже видела этот тест, понимает его смысл, threshold откалиброван на реальных данных.

# было
config:
  severity: warn
# стало
config:
  severity: error
  warn_if: ">1"   # одно нарушение — warn
  error_if: ">5"  # 5+ — error

warn_if/error_if дают tolerance — не каждый одиночный outlier роняет CI.

Этап 4: full coverage (ongoing)

Расширение на staging, intermediate, dimension модели. Здесь обычно достаточно базовых тестов (not_null, accepted_values), без statistical.

Метрика зрелости: процент моделей с хотя бы одним dbt_expectations тестом. Не количество тестов на модель — это легко накрутить. Coverage в смысле «модель защищена» — важнее.


DuckDB nuances

Пакет работает на DuckDB, но есть adapter-specific нюансы:

  1. Типы. DuckDB по умолчанию использует широкие типы — BIGINT для целых, DOUBLE для дробных. expect_column_values_to_be_of_type: INTEGER упадёт, если DuckDB заинференсил BIGINT. Используйте BIGINT явно или model contracts в dbt 1.5+.

  2. Quantile. DuckDB поддерживает quantile_cont(), и пакет диспатчится корректно. Результаты совпадают со Snowflake / BigQuery.

  3. Memory. Statistical тесты на больших таблицах (10M+ rows) могут упереться в memory limit. Решение — where config для subset или SET memory_limit='8GB' в profiles.

  4. Concurrency. DuckDB однопоточный по записи, но многопоточный по чтению. dbt test --threads 8 ускоряет, но контеншн возможен на disk I/O при больших scan’ах.


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

  1. dbt-expectations — порт Great Expectations, 60+ декларативных тестов. Поверх dbt_utils (транзитивная зависимость).
  2. Транзитивная зависимость на dbt_utils — главный source конфликтов. Разрешается явным pinning в packages.yml или секцией overrides.
  3. dbt_utils vs dbt-expectations: utility-пакет vs testing-пакет. Зона пересечения — composite uniqueness, row count equality, expression_is_true. Конвенция команды важнее, чем выбор пакета.
  4. package-lock.yml в git, dbt_packages/ в gitignore. После каждого git pulldbt deps.
  5. Версионирование осторожное. Pin patch range, читайте CHANGELOG перед minor-апгрейдом, обновляйте отдельным PR.
  6. Cost — реальный. 800 тестов на 200-моделей-проекте добавляют 5-10 минут к dbt test. Оптимизация через where, tags, severity, parallelism.
  7. Rollout постепенный. Pilot на 1 модели -> mart-слой -> эскалация severity -> full coverage. Не пытайтесь добавить 50 тестов в один день.
  8. Каталог конкретных тестов — в уроке 7.5 «Тур по dbt-expectations». Здесь — про пакет как зависимость.
Проверка знанийKnowledge check
Команда работает над dbt-проектом 18 месяцев. В `packages.yml` стоит `dbt_utils 1.2.0` (закреплён, потому что в `dbt_project.yml` есть кастомная dispatch на `generate_schema_name`, привязанная к 1.2). Новый middle инженер хочет добавить `dbt_expectations` 0.10.4 для статистических тестов на `fct_orders`. `dbt deps` падает: 'Version error for package dbt-labs/dbt_utils. Found: 1.2.0, required by dbt_expectations: >=1.3.0'. Какие варианты решения, какой выбрать?
ОтветAnswer
Это классический packaging conflict. Разберём четыре варианта по уровню риска и effort. **Вариант 1: upgrade dbt_utils до 1.3.x (рекомендованный)** Простейшее и обычно правильное решение. Обновляем pin: ```yaml packages: - package: dbt-labs/dbt_utils version: 1.3.0 - package: calogica/dbt_expectations version: 0.10.4 ``` Плюсы: - Решает конфликт декларативно, без overrides. - Получаем bugfixes и новые макросы dbt_utils 1.3 (например, улучшенный `union_relations`). - Чисто, без 'технического долга'. Минусы / риски: - Кастомная dispatch на `generate_schema_name` могла зависеть от старого API. Если в dbt_utils 1.3 этот макрос переписали, dispatch перестанет работать. - Любой код проекта, использующий `dbt_utils.something`, нужно проверить на breaking changes. Как выполнять: 1. Прочитать CHANGELOG.md dbt_utils для 1.2 -> 1.3. Особенно секцию 'Breaking changes' и 'Removed/Renamed'. 2. Найти все uses `dbt_utils.` в проекте: `grep -r 'dbt_utils\.' models/ macros/ tests/`. 3. Запустить `dbt deps`, потом `dbt parse` — увидеть, что сломалось. 4. Поправить локально, прогнать `dbt test` на subset. 5. PR с описанием 'Upgrade dbt_utils 1.2 -> 1.3 для совместимости с dbt-expectations'. **Вариант 2: overrides секция (короткий путь, риск выше)** Если upgrade не вариант (например, нет времени аудит делать), можно форсить: ```yaml packages: - package: dbt-labs/dbt_utils version: 1.2.0 - package: calogica/dbt_expectations version: 0.10.4 overrides: - package: dbt-labs/dbt_utils version: 1.2.0 ``` Это говорит dbt: 'ставь 1.2.0, наплевать на требования dbt_expectations'. Плюсы: - Быстро. `dbt deps` пройдёт. - Минимум изменений в проекте. Минусы / риски: - **dbt_expectations может внутри использовать API dbt_utils 1.3.0+**, которого нет в 1.2.0. Конкретные макросы (например, `get_column_values` с новой сигнатурой) упадут на runtime. - Технический долг: вы 'обманули' resolver, но не починили причину. - Команда забудет, что override стоит, и через год кто-то будет долго отлаживать загадочный `UndefinedMacroError`. Когда применять: только как **временное** решение, чтобы разблокировать срочный feature. В backlog задача 'починить override'. **Вариант 3: downgrade dbt_expectations** Искать версию dbt_expectations, совместимую с dbt_utils 1.2.0. Открываем https://github.com/calogica/dbt-expectations/releases, читаем requirements каждой версии. Если существует, например, `dbt_expectations 0.9.0` с требованием `dbt_utils >= 1.2.0`: ```yaml packages: - package: dbt-labs/dbt_utils version: 1.2.0 - package: calogica/dbt_expectations version: 0.9.0 ``` Плюсы: - Никаких изменений в существующем коде. - Совместимость честная, без overrides. Минусы: - Старая версия — нет новых макросов и багфиксов. - Через полгода всё равно придётся апгрейдить, проблема отложена. - Если 0.9 уже EOL — security и compatibility risk. Когда применять: если проект на пороге migration на dbt Fusion / другой адаптер, и команда не хочет инвестировать в текущий tooling. **Вариант 4: не ставить dbt_expectations, написать custom generic** Если нужны статистические тесты только на 1-2 моделях, не тащить весь пакет: ```sql -- tests/generic/test_column_mean_in_range.sql {% test column_mean_in_range(model, column_name, min_value, max_value) %} SELECT 1 FROM {{ model }} HAVING AVG({{ column_name }}) NOT BETWEEN {{ min_value }} AND {{ max_value }} {% endtest %} ``` Использование: ```yaml - name: fct_orders columns: - name: amount data_tests: - column_mean_in_range: min_value: 50 max_value: 200 ``` Плюсы: - Ноль новых зависимостей. - Полный контроль над семантикой теста. - 10 строк кода вместо пакета на 60+ макросов. Минусы: - Не масштабируется. Если потом понадобится `expect_column_quantile_values_to_be_between` — снова писать самим. - Нет ready-made infrastructure (severity / error_if / store_failures работают, но как для любого custom теста). Когда применять: если статистических тестов нужно 1-3, и команда не хочет тащить пакет. **Рекомендация по контексту вопроса** Если команда добавляет тесты на `fct_orders` ради нескольких метрик (mean amount, quantile), скорее всего понадобится 5-10 разных макросов в течение года — пакет окупится. Выбор: **Вариант 1**. Конкретный план: 1. Сначала аудит кастомной dispatch `generate_schema_name`. Если она использует internal-API dbt_utils (что редко), смотрим CHANGELOG. 2. Скорее всего dispatch вообще не зависит от dbt_utils — это часть dbt-core. Проверяем, что в `macros/` оверрайдится только `generate_schema_name`, без других dbt_utils-зависимостей. 3. Если dispatch стандартная, апгрейд dbt_utils 1.2 -> 1.3 безопасен — ломающих изменений между этими версиями не было. 4. Обновляем pin, делаем PR, запускаем CI, мержим. Если почему-то нельзя апгрейдить (компании-specific governance) — **Вариант 4** на короткой дистанции (custom generic для одного теста), потом плановый апгрейд dbt_utils в следующем квартале. **Вариант 2** (overrides) — никогда без понимания, что вы делаете. Это эскейп-хатч для аварийных ситуаций, не для production rollout новой фичи. **Принцип**: packaging-конфликт почти всегда решается **upgrade**, не **override**. Override откладывает проблему, и за ней следует молчаливый runtime-баг. Если upgrade невозможен — это сигнал, что в проекте есть deeper coupling, который нужно distangle отдельной задачей.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Какие 3 уровня тестов в dbt-expectations и для чего каждый?

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

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

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

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