Любая модель в dbt без тестов — это бомба замедленного действия. Когда логика растёт, рефакторится, появляются новые сценарии — без тестов ты не узнаешь, что сломал, пока не позвонят с продакшна “почему в дашборде revenue ушёл в минус”.
dbt из коробки даёт четыре теста, которые покрывают 80% базовых data-quality проверок: not_null, unique, accepted_values, relationships. В этом уроке разбираемся, что каждый делает физически в warehouse, как их подключить и зачем они нужны.
Что такое generic-тест в dbt
Generic test — это параметризованный SQL-запрос, который dbt применяет к колонке (или модели). Тест считается успешным, если запрос возвращает 0 строк. Если возвращает > 0 — это считается провалом теста, потому что найдены строки, нарушающие проверку.
Тест компилируется в SELECT-запрос. Если result_count = 0 — PASS. Если > 0 — FAIL, возвращённые строки — это нарушители. dbt считает count и сравнивает с 0.
Все 4 встроенных теста работают по этой логике — отличается только параметризованный SELECT.
not_null: проверка на NULL
Простейший тест. Проверяет, что в колонке нет NULL-значений.
version: 2
models:
- name: dim_customers
columns:
- name: customer_id
tests:
- not_null
Компилируется в (приблизительно):
SELECT customer_id
FROM dim_customers
WHERE customer_id IS NULL
Если хотя бы одна строка вернётся (customer_id есть NULL) — тест провалится.
Когда применять:
- Primary keys (всегда).
- Foreign keys (если связь обязательна).
- Бизнес-критичные поля (status, amount, timestamp).
- Поля, которые потом используются в JOIN — NULL в JOIN-ключе ломает связь.
Когда НЕ применять:
- Optional поля, где NULL — валидное значение (например,
deleted_at— есть только у удалённых). - Колонки, которые точно могут быть NULL по дизайну.
unique: проверка на уникальность
Проверяет, что значения в колонке не повторяются.
models:
- name: dim_customers
columns:
- name: customer_id
tests:
- unique
Компилируется в:
SELECT customer_id
FROM dim_customers
GROUP BY customer_id
HAVING COUNT(*) > 1
Если хотя бы один customer_id появляется более одного раза — тест провалится.
Когда применять:
- Primary keys (всегда).
- Surrogate keys (генерированные хэши).
- Уникальные business identifiers (email, ssn).
Когда НЕ применять:
- Foreign keys (по определению неуникальны).
- Обычные атрибуты (country, status, amount).
- Columns с легитимными дублями.
Самая частая ошибка junior: повесить unique на колонку, которая по дизайну может повторяться. Например, на customer_id в orders — но один клиент делает много заказов, поэтому customer_id повторяется. Тест будет каждый раз падать, ничего не сломано. Подумай: эта колонка действительно уникальна на уровне строк модели?
accepted_values: проверка на список допустимых значений
Проверяет, что колонка содержит только значения из заданного списка.
models:
- name: fact_orders
columns:
- name: status
tests:
- accepted_values:
values: ['pending', 'shipped', 'delivered', 'cancelled']
Компилируется в:
SELECT status
FROM fact_orders
WHERE status NOT IN ('pending', 'shipped', 'delivered', 'cancelled')
OR status IS NULL -- зависит от версии dbt
Если есть строки со значениями вне списка — провалится.
Когда применять:
- Enum-like поля (status, type, category, tier).
- Country codes (если ограниченный список).
- Currency codes.
- Любые поля с фиксированным набором значений.
Параметр quote: для нестринговых типов (числа, boolean) указывай quote: false:
- accepted_values:
values: [1, 2, 3, 4, 5]
quote: false
Это контролирует, обворачивать ли значения в кавычки в скомпилированном SQL. Для строк — true (default), для чисел — false.
relationships: проверка foreign key
Проверяет, что все значения колонки A в текущей модели существуют в колонке B другой модели. Эквивалент foreign key constraint.
models:
- name: fact_orders
columns:
- name: customer_id
tests:
- relationships:
to: ref('dim_customers')
field: id
Компилируется в:
SELECT customer_id
FROM fact_orders
WHERE customer_id IS NOT NULL
AND customer_id NOT IN (
SELECT id FROM dim_customers
)
Если в fact_orders есть customer_id, которого нет в dim_customers — провалится.
Когда применять:
- Foreign keys между fact и dim таблицами.
- Между staging и source.
- Между моделями, где должны быть строгие связи.
Параметр field: колонка в родительской модели, к которой ссылается. Может отличаться от имени в текущей модели.
- relationships:
to: ref('dim_customers')
field: customer_id # имя в родительской модели
Все тесты можно комбинировать на одной колонке
Часто на primary key вешают и not_null, и unique:
models:
- name: dim_customers
columns:
- name: customer_id
tests:
- not_null
- unique
И это валидно: dbt выполнит два теста параллельно. Логика “primary key должен быть not null + unique” — стандартная.
На foreign key обычно not_null + relationships:
- name: fact_orders
columns:
- name: customer_id
tests:
- not_null
- relationships:
to: ref('dim_customers')
field: customer_id
Тесты можно вешать на sources
Не только на модели. На source-таблицы (загружаемые внешними loaders) тоже:
sources:
- name: jaffle_shop
tables:
- name: raw_customers
columns:
- name: id
tests:
- not_null
- unique
dbt test --select source:jaffle_shop запустит тесты на raw-данных. Полезно как раннее обнаружение проблем — если loader сломался, тест упадёт сразу, ещё до staging.
Запуск тестов
# Все тесты в проекте
dbt test
# Тесты конкретной модели
dbt test --select dim_customers
# Тесты для одной колонки (через тэг или путь)
dbt test --select dim_customers,test_type:not_null
Output:
14:23:01 Running tests
14:23:01 Concurrency: 4 threads
14:23:01 1 of 4 START test not_null_dim_customers_customer_id .... [RUN]
14:23:01 2 of 4 START test unique_dim_customers_customer_id ...... [RUN]
14:23:02 1 of 4 PASS test not_null_dim_customers_customer_id ..... [PASS in 0.18s]
14:23:02 2 of 4 PASS test unique_dim_customers_customer_id ....... [PASS in 0.22s]
Done. PASS=2 WARN=0 ERROR=0 SKIP=0 TOTAL=2
Если хоть один FAIL — exit code 1, в CI это блокирует pipeline.
Команда dbt build: run + test вместе
dbt build — это dbt run + dbt test, но с правильным порядком: если тесты модели проваливаются, downstream-модели не запустятся. Это безопасный default для production:
dbt build
Логика: dbt сначала строит модель, затем сразу тестирует её. Если PASS — продолжает downstream. Если FAIL — стопает ветку DAG, чтобы не строить модели на плохих данных.
Анатомия скомпилированного теста
После dbt test файлы тестов лежат в target/compiled/<project>/models/...:
target/compiled/jaffle_shop/models/_models.yml/not_null_dim_customers_customer_id.sql
Содержимое:
SELECT customer_id
FROM jaffle_shop.main.dim_customers
WHERE customer_id IS NULL
Это и есть скомпилированный тест. dbt оборачивает его при выполнении: считает count(*). Если 0 — PASS. Открой target/run/... чтобы увидеть финальную обёртку.
DuckDB-специфика для тестов
В DuckDB тесты работают так же, как везде:
- SELECT-запросы быстрые — даже на миллионах строк uniqueness/not_null проверяется за секунды.
- Параллелизм через threads — в profiles.yml threads: 4 запустит до 4 тестов одновременно.
- Limit на single-writer не задевает тесты — это read-only операции.
dbt testна пустой таблице — PASS для всех тестов (нет строк = нет нарушителей).
Команды для отладки
# Только тесты, без построения моделей
dbt test --select dim_customers
# Тесты + посмотреть скомпилированный SQL
dbt compile --select dim_customers
cat target/compiled/jaffle_shop/models/.../not_null_*.sql
# Тесты с storage failures (см. урок 4)
dbt test --select dim_customers --store-failures
Попробуй сам
В своём проекте создай models/marts/_models.yml:
version: 2
models:
- name: dim_customers
columns:
- name: customer_id
tests:
- not_null
- unique
- name: email
tests:
- not_null
Если у тебя есть модель dim_customers — запусти:
dbt test --select dim_customers
Получишь PASS/FAIL для каждого теста.
Теперь “сломай” модель: в SQL добавь UNION с дубликатом одной строки. Запусти dbt run + dbt test. Увидишь FAIL на unique:
14:23:02 1 of 1 FAIL 1 unique_dim_customers_customer_id ......... [FAIL 1 in 0.31s]
FAIL 1 означает: 1 строка вернулась из SELECT — это нарушитель. Открой target/compiled/.../unique_*.sql и выполни вручную в DuckDB — увидишь дубль.
Что мы поняли
Четыре встроенных generic-теста: not_null (нет NULL), unique (нет повторов), accepted_values (только заданные значения), relationships (foreign key). Каждый — параметризованный SELECT, PASS = 0 строк. Можно комбинировать на одной колонке. Вешать на модели И на sources. dbt test запускает их, dbt build делает run + test атомарно. Покрывают 80% базовых data-quality проверок.
В следующем уроке разберём, когда какой тест применять, особенно различия между staging-слоем и marts.
Constraints в SQL: PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE Где валидировать данные: source, ingestion или warehouse