Learning Platform
Глоссарий Troubleshooting
Урок 09.01 · 18 мин
Начальный
generic testsnot_nulluniqueaccepted_valuesrelationships

Любая модель в dbt без тестов — это бомба замедленного действия. Когда логика растёт, рефакторится, появляются новые сценарии — без тестов ты не узнаешь, что сломал, пока не позвонят с продакшна “почему в дашборде revenue ушёл в минус”.

dbt из коробки даёт четыре теста, которые покрывают 80% базовых data-quality проверок: not_null, unique, accepted_values, relationships. В этом уроке разбираемся, что каждый делает физически в warehouse, как их подключить и зачем они нужны.

Что такое generic-тест в dbt

Generic test — это параметризованный SQL-запрос, который dbt применяет к колонке (или модели). Тест считается успешным, если запрос возвращает 0 строк. Если возвращает > 0 — это считается провалом теста, потому что найдены строки, нарушающие проверку.

Как работает generic-тест в dbt

Тест компилируется в SELECT-запрос. Если result_count = 0 — PASS. Если > 0 — FAIL, возвращённые строки — это нарушители. dbt считает count и сравнивает с 0.

not_null test on emailпараметризованный шаблон
Скомпилированный SQLSELECT * FROM customers WHERE email IS NULL
Result0 строк = PASS
Result> 0 строк = FAIL

Все 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 с легитимными дублями.
WARNING

Самая частая ошибка 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 тесты работают так же, как везде:

  1. SELECT-запросы быстрые — даже на миллионах строк uniqueness/not_null проверяется за секунды.
  2. Параллелизм через threads — в profiles.yml threads: 4 запустит до 4 тестов одновременно.
  3. Limit на single-writer не задевает тесты — это read-only операции.
  4. 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
Проверка знанийKnowledge check
Ты повесил unique на колонку customer_id в модели fact_orders. dbt test падает с FAIL — есть дубли. Но логически это правильно: один клиент делает много заказов. Что сделал не так и как починить?
ОтветAnswer
unique нужно вешать на колонку, которая уникальна на уровне СТРОК модели. В fact_orders каждая строка = один заказ, поэтому уникальный ключ — order_id, а не customer_id (один клиент может иметь много заказов, поэтому customer_id повторяется по дизайну). Решение: переместить unique на order_id; на customer_id оставить not_null (если заказ всегда должен иметь клиента) + relationships к dim_customers (для проверки foreign key). unique применяется только к primary/surrogate keys, не к foreign.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Что означает PASS у теста в dbt?

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

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

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

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