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 наиболее ценных макросов с примерами кода.
Установка и зависимости
Стандартный 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 явно.
На практике:
# 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 важно понимать, для чего каждый предназначен.
Сценарии:
Нужны оба. Типично для зрелого проекта с 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-core | dbt_utils | dbt-expectations |
|---|---|---|---|
| Уникальная пара колонок | — | unique_combination_of_columns | expect_compound_columns_to_be_unique |
| Равенство row count | — | equal_rowcount | expect_table_row_count_to_equal_other_table |
| Условие на строки | — | expression_is_true | expect_row_values_to_have_data_for_every_n_datepart |
| not_null | not_null | — | expect_column_values_to_not_be_null |
| accepted_values | accepted_values | — | expect_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, иначе nativemodel 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 с этим макросом нужно переписать.
Алгоритм:
- Не обновляйте версию вслепую. Перед апгрейдом откройте
https://github.com/calogica/dbt-expectations/blob/main/CHANGELOG.mdи прочитайте секцию для целевой версии. - Создайте feature branch. Изменения версии — отдельный PR, не смешивайте с feature-работой.
- Pin новую версию точечно.
version: 0.10.4явно, без range. - Запустите
dbt parse --select state:modified+. Это покажет, какие модели/тесты используют изменившиеся макросы. - Обновляйте YAML итеративно. Запускайте
dbt parseпосле каждой партии изменений. - Прогон
dbt test. Семантика теста может поменяться — например,expect_column_values_to_be_betweenв новой версии включает границы, а в старой исключала. - Merge. После зелёного CI и code review.
Обновлять dbt-expectations редко — не чаще раза в квартал. Между апгрейдами добавляйте только новые тесты на текущей версии, не лезьте в старые ради «новой фичи».
Cost: каждый expect_* — это SQL
Декларативность пакета скрывает простую правду: каждый макрос компилируется в отдельный SELECT с агрегатами. На крупном проекте это серьёзная статья в стоимости dbt test.
Контрольные цифры (грубо, 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 нюансы:
-
Типы. DuckDB по умолчанию использует широкие типы —
BIGINTдля целых,DOUBLEдля дробных.expect_column_values_to_be_of_type: INTEGERупадёт, если DuckDB заинференсилBIGINT. ИспользуйтеBIGINTявно илиmodel contractsв dbt 1.5+. -
Quantile. DuckDB поддерживает
quantile_cont(), и пакет диспатчится корректно. Результаты совпадают со Snowflake / BigQuery. -
Memory. Statistical тесты на больших таблицах (10M+ rows) могут упереться в memory limit. Решение —
whereconfig для subset илиSET memory_limit='8GB'в profiles. -
Concurrency. DuckDB однопоточный по записи, но многопоточный по чтению.
dbt test --threads 8ускоряет, но контеншн возможен на disk I/O при больших scan’ах.
Ключевые выводы
- dbt-expectations — порт Great Expectations, 60+ декларативных тестов. Поверх
dbt_utils(транзитивная зависимость). - Транзитивная зависимость на dbt_utils — главный source конфликтов. Разрешается явным pinning в
packages.ymlили секциейoverrides. - dbt_utils vs dbt-expectations: utility-пакет vs testing-пакет. Зона пересечения — composite uniqueness, row count equality, expression_is_true. Конвенция команды важнее, чем выбор пакета.
- package-lock.yml в git, dbt_packages/ в gitignore. После каждого
git pull—dbt deps. - Версионирование осторожное. Pin patch range, читайте CHANGELOG перед minor-апгрейдом, обновляйте отдельным PR.
- Cost — реальный. 800 тестов на 200-моделей-проекте добавляют 5-10 минут к
dbt test. Оптимизация черезwhere,tags,severity, parallelism. - Rollout постепенный. Pilot на 1 модели -> mart-слой -> эскалация severity -> full coverage. Не пытайтесь добавить 50 тестов в один день.
- Каталог конкретных тестов — в уроке 7.5 «Тур по dbt-expectations». Здесь — про пакет как зависимость.