В прошлых уроках мы научились писать тесты и контролировать их severity. Но когда тест провалился, важный вопрос: какие именно строки его нарушают? Без понимания нарушителей дебаг сложен.
dbt даёт механизм store_failures: сохраняет провалившие тест строки в отдельную таблицу в warehouse. Это и есть инструмент для дебага data-quality проблем.
Что делает store_failures
По умолчанию dbt просто запускает тест и считает COUNT строк-нарушителей. Если 0 — PASS, если > 0 — FAIL. Сами нарушители теряются.
С store_failures: true dbt дополнительно делает: материализует результат теста в таблицу. То есть скомпилированный SQL теста — это просто SELECT, и dbt оборачивает его в CREATE OR REPLACE TABLE.
Без: dbt считает count, либо PASS либо FAIL. Со store_failures: dbt создаёт таблицу с failed-строками, ты можешь их прочитать и понять, что нарушает.
Включение через CLI
Самый простой способ — флаг --store-failures:
dbt test --select dim_customers --store-failures
После выполнения в warehouse появятся таблицы вида:
<target_schema>_dbt_test__audit.not_null_dim_customers_customer_id
<target_schema>_dbt_test__audit.unique_dim_customers_customer_id
Внутри них — строки, которые провалили тест. Можно их прочитать обычным SELECT.
Включение через config
В YAML на уровне теста:
- name: customer_id
tests:
- not_null:
config:
store_failures: true
Или для всех тестов в проекте через dbt_project.yml:
tests:
+store_failures: true
Когда задано через config, флаг --store-failures не нужен — store_failures всегда применяется.
Что в таблице failed_rows
Структура таблицы зависит от теста. Для not_null:
SELECT customer_id
FROM dim_customers
WHERE customer_id IS NULL
Скомпилированное в storage. В таблице — все колонки SELECT, в данном случае один customer_id (NULL).
Для unique:
SELECT customer_id
FROM dim_customers
GROUP BY customer_id
HAVING COUNT(*) > 1
В таблице — список customer_id, которые дублируются.
Для accepted_values:
SELECT field
FROM model
WHERE field NOT IN ('a', 'b', 'c')
В таблице — невалидные значения.
Для relationships:
SELECT customer_id
FROM fact_orders
WHERE customer_id IS NOT NULL
AND customer_id NOT IN (SELECT id FROM dim_customers)
В таблице — customer_id из fact_orders, которых нет в dim_customers.
Где найти failed_rows таблицы
Стандартное расположение:
<target.schema>_dbt_test__audit.<test_name>
Например:
main_dbt_test__audit.not_null_dim_customers_customer_id
dbt автоматически создаёт схему <schema>_dbt_test__audit (с суффиксом), чтобы не загромождать main schema.
В DuckDB можно посмотреть:
duckdb jaffle_shop.duckdb -c "SELECT * FROM main_dbt_test__audit.not_null_dim_customers_customer_id;"
Дебаг workflow
Типовой сценарий:
-
dbt testпадает с FAIL. Например:FAIL 5 unique_dim_customers_customer_id [...]5 нарушителей.
-
Включить store_failures:
dbt test --select dim_customers --store-failures -
Прочитать failed rows:
SELECT * FROM main_dbt_test__audit.unique_dim_customers_customer_id;Получишь 5 customer_id с дублями.
-
Дальше JOIN на оригинальную модель:
SELECT * FROM dim_customers WHERE customer_id IN ( SELECT customer_id FROM main_dbt_test__audit.unique_dim_customers_customer_id );Увидишь полные дубликаты — две строки с тем же customer_id, разными атрибутами. Это даёт контекст: например, обнаружишь, что loader записал customer дважды с разным email.
store_failures особенно полезен для relationships-тестов. Если 1000 customer_id в fact_orders не существуют в dim_customers, ты не угадаешь, какие именно. С store_failures получаешь точный список — и можешь решить, чинить ли источник или менять логику staging.
store_failures_as (dbt 1.9+)
В dbt 1.9 добавили параметр store_failures_as, который контролирует как именно хранить failed-строки. Варианты:
table(default до 1.9) — обычная table.view— view с SELECT нарушителей. Всегда свежая, не занимает место.ephemeral— НЕ поддерживается (это значит “не хранить”).
- name: customer_id
tests:
- not_null:
config:
store_failures: true
store_failures_as: view
Почему view может быть лучше:
- Не занимает storage (особенно важно на больших faild_rows).
- Всегда свежая — отражает текущее состояние модели, не зафиксированное на момент dbt test.
- Не нужно повторять dbt test для обновления.
Почему table:
- Историческая запись — могу посмотреть, что было нарушено вчера.
- View будет пересчитываться при каждом SELECT — на больших моделях может тормозить.
Параметры количества failed rows
В compiled SQL теста стандартно нет LIMIT — dbt считает все нарушители. Но в store_failures можно ограничить:
- name: customer_id
tests:
- not_null:
config:
store_failures: true
# store_failures обычно сохраняет все, но в config теста можно
# добавить limit через custom test или generic с фильтром
В практике достаточно сохранять всё — даже миллион failed rows занимает мало места. Если нарушителей миллион, у тебя проблема не в storage, а в данных.
Production использование
В prod-проектах store_failures часто всегда включен через dbt_project.yml:
tests:
+store_failures: true
+store_failures_as: view # в 1.9+
Преимущества:
- Любой провал теста сразу даёт контекст для дебага.
- В CI можно сделать parsing failed_rows таблиц и отправлять в Slack/PagerDuty.
- При расследовании инцидентов не нужно повторно запускать dbt test с флагом.
Минусы:
- Каждый тест с store_failures чуть дольше выполняется (нужно создать таблицу).
- На warehouse, где storage стоит денег (Snowflake), это +N таблиц. На DuckDB бесплатно.
DuckDB-специфика для store_failures
В DuckDB store_failures работает быстро:
- Таблица failed_rows создаётся через CREATE OR REPLACE TABLE — атомарно, single-writer.
- Storage в одном файле .duckdb — нет per-table cost.
store_failures_as: viewтоже supported.- Audit-схема
<schema>_dbt_test__auditсоздаётся автоматически.
На DuckDB можно безопасно делать +store_failures: true для всего проекта — overhead минимальный.
Команды для дебага
# Включить через CLI
dbt test --store-failures
# Selective test с store_failures
dbt test --select dim_customers --store-failures
# Посмотреть схему с audit таблицами
duckdb jaffle_shop.duckdb -c "SHOW TABLES FROM main_dbt_test__audit;"
# Прочитать failed rows
duckdb jaffle_shop.duckdb -c "SELECT * FROM main_dbt_test__audit.not_null_dim_customers_customer_id LIMIT 10;"
Попробуй сам
Создай модель с заведомо плохими данными:
-- models/staging/stg_test_failures.sql
SELECT 1 AS id, 'a' AS status
UNION ALL
SELECT 1 AS id, 'b' AS status -- дубль id
UNION ALL
SELECT NULL AS id, 'c' AS status -- NULL id
YAML:
models:
- name: stg_test_failures
columns:
- name: id
tests:
- not_null
- unique
Запусти:
dbt run --select stg_test_failures
dbt test --select stg_test_failures --store-failures
Увидишь FAIL на оба теста. Проверь:
duckdb jaffle_shop.duckdb -c "SELECT * FROM main_dbt_test__audit.not_null_stg_test_failures_id;"
duckdb jaffle_shop.duckdb -c "SELECT * FROM main_dbt_test__audit.unique_stg_test_failures_id;"
Получишь конкретные нарушители: NULL для not_null, id=1 (с count 2) для unique.
Что мы поняли
store_failures — механизм сохранения failed-строк тестов в отдельную таблицу/view. Включается через CLI флаг --store-failures или через config в YAML/dbt_project.yml. Создаёт audit-схему <schema>_dbt_test__audit с таблицами по имени теста. В dbt 1.9+ есть store_failures_as для выбора между table и view. На production удобно включать глобально — overhead минимальный, контекст для дебага огромный.
На этом мы закончили модуль про базовые тесты. В следующем модуле — singular, custom generic и unit tests, которые покрывают сценарии вне четырёх встроенных.
store_failures и store_failures_as: production debugging workflow