Условие
«У нас в выручке за вчера резкий провал» — звонит CFO утром. На самом деле — ETL упал ночью, и витрина обновлена только наполовину. Какие data-quality тесты надо было поставить, чтобы CFO не увидел поломанную цифру? Опишите 6+ типов проверок и приведите код в dbt.
Решение
Категории проверок
- Свежесть (freshness) — данные обновились?
- Полнота (completeness) — все ли строки/партиции на месте?
- Уникальность (uniqueness) — нет ли дублей по PK?
- Ссылочная целостность — все ли FK существуют в dim?
- Диапазон значений — amount > 0, status в списке валидных?
- Аномалии — резкие отклонения от вчера/прошлой недели?
- Не-null — обязательные поля заполнены?
- Распределение — доля каждого segment не уехала?
Реализация в dbt
# models/marts/orders_daily.yml
version: 2
models:
- name: orders_daily
description: "Ежедневная агрегация заказов"
tests:
- dbt_utils.equal_rowcount: # сравниваем с источником
compare_model: ref('stg_orders_count_daily')
columns:
- name: dt
tests:
- unique
- not_null
- dbt_utils.expression_is_true:
expression: ">= '2020-01-01'"
- name: n_orders
tests:
- not_null
- dbt_utils.expression_is_true:
expression: "> 0"
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 10000000
- name: gmv
tests:
- not_null
- dbt_utils.expression_is_true:
expression: ">= 0"
- name: status
tests:
- accepted_values:
values: ['paid', 'refunded', 'cancelled']
sources:
- name: raw
freshness:
warn_after: {count: 4, period: hour}
error_after: {count: 12, period: hour}
loaded_at_field: ingested_at
tables:
- name: ordersАномалии (anomaly detection)
-- models/dq/orders_daily_anomalies.sql
WITH stats AS (
SELECT AVG(n_orders) AS mu, STDDEV(n_orders) AS sd
FROM {{ ref('orders_daily') }}
WHERE dt BETWEEN CURRENT_DATE - 30 AND CURRENT_DATE - 1
)
SELECT dt, n_orders, mu, sd, (n_orders - mu) / sd AS z_score
FROM {{ ref('orders_daily') }}, stats
WHERE dt = CURRENT_DATE - 1
AND ABS((n_orders - mu) / sd) > 3;tests:
- dbt_utils.equal_rowcount:
compare_model: ref('orders_daily_anomalies')
where: "dt = current_date - 1"
expression: "= 0" # никаких аномалий за вчераOperational: где запускать
# CI on PR
dbt build --select state:modified+ --defer --state ./prod-manifest
# nightly
dbt source freshness && dbt build --select tag:nightlyПри падении теста — Airflow / GitHub Action отправляет алерт в Slack, витрина не публикуется.
Контракт: "не показывать сырые данные"
В Airflow:
copy >> refresh >> dq_check >> publish_viewpublish_view пересоздаёт mart.orders_daily_v_public только если dq_check зелёный. Иначе пользователи видят вчерашние данные с пометкой «stale».
Подводные камни
- Freshness на основе
MAX(created_at)— обманчив: пайплайн может работать, но не приходят свежие события. Нужно мониторитьMAX(ingested_at). uniqueтест на 1 млрд строк — дорого; запускать на инкременте.- Hard fail vs warn: критические проверки (
unique, FK, диапазоны) — error; soft (статистика) — warn. - Cnt-сравнение
source vs target: учитывайте late-arriving — допуск ±0.5%. - Алерт-шум: 50 фейлов в день = игнор. Алертить только по критичным тестам.
- «Тесты только на staging» — забудьте; mart-слой надо тестировать максимально.
- Контрактные тесты (
dbt contract) — фиксируют типы и порядок колонок, защищают от breaking changes.
Эталонный ответ
Обязательный набор DQ-тестов на витрине: freshness (макс возраст данных), uniqueness PK, not_null, accepted_values, диапазоны (gmv ≥ 0), referential integrity к dim, anomaly detection (z-score / WoW), row-count vs source. В dbt: tests: блок + dbt_utils + dbt_expectations + source freshness. Публикацию витрины делать через отдельный шаг, который запускается только при зелёных тестах.