dbt tests на собеседовании Data Engineer

Проверь себя · 1/3разбор после ответа
Для двустороннего теста при уровне значимости 0.05 построен 95% доверительный интервал для эффекта: [-0.2; 0.1]. Что следует сказать о проверке H0: delta = 0?

Почему dbt tests спрашивают

Тесты — это то, что отличает «накидал SQL-моделей» от «построил надёжный DWH». На собесе Data Engineer вопрос про dbt tests проверяет, понимаете ли вы разницу между кодом, который просто работает, и кодом, которому можно доверять в проде. Если пайплайн молча пропускает дубли или NULL там, где их быть не должно, дашборды врут, а бизнес принимает решения по кривым цифрам — и это уже не техническая проблема, а репутационная.

Механика простая: dbt-тест — это SQL-запрос, который должен вернуть ноль строк. Каждая вернувшаяся строка — это нарушение. dbt test прогоняет все тесты, а dbt build чередует построение моделей и их проверку, чтобы битые данные не поехали дальше по DAG. На собесе обычно просят разобрать четыре вещи: встроенные generic-тесты, singular-тесты, свои переиспользуемые тесты и настройку критичности (severity). Разберём по порядку.

Generic tests

Generic-тесты (их же называют schema-тестами) — это готовые проверки, которые вешаются на колонку прямо в YAML. Из коробки в dbt их четыре, и они закрывают львиную долю ежедневных проверок качества:

  • unique — в колонке нет дублей. Классика для первичных ключей.
  • not_null — в колонке нет NULL. Проверяет обязательные поля.
  • accepted_values — значения из заранее заданного списка. Ловит опечатки в статусах и категориях.
  • relationships — ссылочная целостность (FK): каждое значение есть в родительской таблице.
columns:
  - name: id
    tests: [unique, not_null]
  - name: status
    tests:
      - accepted_values:
          values: ['active', 'inactive']
  - name: customer_id
    tests:
      - relationships:
          to: ref('customers')
          field: id

Под капотом каждый такой тест dbt компилирует в обычный SELECT. Например, not_null превращается в SELECT * FROM model WHERE id IS NULL: вернулись строки — тест не прошёл. Это удобно объяснять на собесе: никакой магии, просто сгенерированный запрос, который ищет нарушения.

Singular tests

Singular-тесты — это одноразовые проверки под конкретную бизнес-логику, которую не выразить готовыми generic-тестами. Пишутся как обычные .sql-файлы в папке tests/ и представляют собой запрос, выбирающий «плохие» строки.

-- tests/no_negative_amounts.sql
SELECT *
FROM {{ ref('orders') }}
WHERE amount < 0;

Правило то же: тест проходит, если запрос вернул ноль строк, и падает, если вернул хотя бы одну. Singular-тесты хороши, когда проверка привязана к одной конкретной модели — например, «сумма заказа не может быть отрицательной» или «дата отгрузки не раньше даты заказа». Но если такую же логику хочется применить к десятку моделей, копипастить SQL — плохая идея. Тогда её выносят в custom generic test.

Custom generic tests

Custom generic test — это ваш собственный переиспользуемый тест, который можно навесить на любую колонку в любой модели. Определяется через блок {% test %} (обычно в macros/ или tests/generic/), а внутри — тот же запрос, выбирающий нарушения.

-- macros/test_positive_value.sql
{% test positive_value(model, column_name) %}
SELECT *
FROM {{ model }}
WHERE {{ column_name }} <= 0
{% endtest %}

Дальше он применяется так же, как встроенный:

columns:
  - name: amount
    tests:
      - positive_value

Смысл — DRY: один раз описали проверку «значение должно быть положительным» и переиспользуете её во всех моделях, где это нужно. На собесе именно это отличает мидла от джуна: джун напишет десять singular-тестов, мидл вынесет логику в один параметризованный generic-тест.

Severity и пороги

По умолчанию упавший тест — это ошибка (severity: error), которая роняет весь прогон. Но не каждое нарушение критично: небольшое расхождение в данных иногда допустимо и не должно останавливать пайплайн. Для этого есть уровни критичности и пороги.

columns:
  - name: email
    tests:
      - unique:
          severity: warn  # или error
          warn_if: ">100"
          error_if: ">1000"
  • error — прогон падает, данные дальше не едут. Ставится на всё, что ломает продукт: битые PK, потерянные FK.
  • warn — dbt пишет предупреждение в лог, но продолжает. Подходит для метрик качества, где небольшой дрейф терпим.
  • warn_if / error_if — пороги по числу вернувшихся строк. В примере выше: до 100 нарушений — тихо, от 100 — предупреждение, от 1000 — падение.

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

Подготовься к собесу по A/B и статистике
300+ вопросов с разбором: дизайн, размер выборки, p-value, ловушки
Тренировать A/B в Telegram

store_failures

Когда тест падает на большом датасете, «упал unique на orders.id» — бесполезное сообщение: непонятно, какие именно строки виноваты. Флаг store_failures сохраняет сами нарушившие строки в отдельную аудиторскую таблицу, чтобы их можно было разобрать позже.

tests:
  - unique:
      store_failures: true

Провалившие строки складываются в схему dbt_test__audit (имя настраивается), и к ним можно обратиться обычным запросом:

SELECT * FROM analytics.dbt_test__audit.unique_orders_id;

Это критично для отладки тестов на больших таблицах: вместо «что-то не так с 12 000 строк» вы сразу видите конкретные дубли и понимаете, откуда они взялись — из источника, из джойна или из неверной дедупликации.

Как это спрашивают на собесе

Формулировки, которые реально звучат на интервью, и что интервьюер хочет услышать:

  • «Чем singular-тест отличается от generic?» Generic параметризуется и переиспользуется на разных моделях и колонках; singular — это разовый SQL под конкретную модель. Ответ проверяет, понимаете ли вы, когда выносить логику в макрос.
  • «Тест упал в 3 ночи, но данные почти корректны — как сделать, чтобы пайплайн не падал?» Понизить severity до warn или выставить error_if с адекватным порогом, а не отключать тест совсем.
  • «Как понять, из-за чего именно упал тест на таблице в 50 млн строк?» Включить store_failures и посмотреть аудиторскую таблицу с конкретными нарушениями.
  • «Где вешать not_null и unique — на staging или на marts?» Обычно и там, и там: на staging ловим проблемы источника раньше, на marts гарантируем контракт для BI.

Интервьюер смотрит не на знание синтаксиса, а на инженерное мышление: понимаете ли вы, что тест — это способ поймать проблему до того, как её увидит бизнес.

Частые ошибки

  • Тесты только на marts. Если проверять данные только в самом конце, отладка превращается в кошмар: непонятно, на каком слое сломалось. Тестируйте и staging тоже.
  • Всё на severity: error. Тогда любой мелкий дрейф роняет весь прогон, команда привыкает к красным прогонам и перестаёт на них реагировать. Разделяйте критичное и терпимое.
  • unique и not_null только на PK. Ссылочную целостность (relationships) и допустимые значения (accepted_values) забывают, а именно они ловят разъехавшиеся джойны и мусорные статусы.
  • Падение без store_failures на больших таблицах. Тест краснеет, а что чинить — непонятно. Для тяжёлых моделей включайте сохранение нарушений заранее.
  • Копипаст singular-тестов вместо custom generic. Десять почти одинаковых SQL-файлов — сигнал, что пора вынести проверку в переиспользуемый макрос.

Связанные темы

FAQ

Чем singular-тест отличается от generic?

Singular — это разовый SQL-запрос в папке tests/ под конкретную модель. Generic (встроенный или свой) параметризуется через model и column_name и переиспользуется на любых колонках. Если одну и ту же проверку хочется навесить на несколько моделей — это generic.

Что происходит при падении теста?

По умолчанию тест с severity: error роняет прогон, и с dbt build зависимые модели дальше не строятся — битые данные не едут в прод. Тест с severity: warn только пишет предупреждение в лог и не останавливает пайплайн.

Как посмотреть, какие именно строки не прошли тест?

Включить store_failures: true. Тогда нарушившие строки сохраняются в аудиторскую таблицу (схема dbt_test__audit по умолчанию), и их можно разобрать обычным SELECT — это спасает при отладке тестов на больших датасетах.

Тесты замедляют прогон, что делать?

Тест — это дополнительный запрос, так что на больших таблицах они стоят времени. Помогает: тестировать критичное на staging (раньше и на меньших объёмах), использовать where-конфиг для проверки только свежей партиции, разумно расставлять severity, чтобы не гонять тяжёлые проверки на каждый чих.

Это официальная информация?

Нет. Статья основана на документации dbt (1.7+) и опыте кандидатов. Конкретный набор тестов и соглашения зависят от команды и проекта.


Тренируйте Data Engineering — откройте тренажёр с 1500+ вопросами для собесов.