Тема: Часть 11. Проверка данных: validation.md
Уровень сложности: Средний
Расчётное время изучения: 2-3 часа
Предварительные требования: Базовое понимание dbt (модели, тесты, schema.yml)
Знание SQL на уровне написания запросов и singular-тестов
Знакомство со спецификациями ODCS и ODPS
Понимание концепции Specification-Driven Development (SDD)
Опыт работы с командной строкой и запуском dbt-команд
Цели обучения: Сформулировать проверочный факт в виде команды, SQL-запроса или ручного шага ревьюера, отличая его от пожелания
Разделить факты на автоматические и ручные, обосновав выбор типа для каждого
Создать минимальный validation.md для витрины, включающий факты о grain, PII, контракте, lineage и ручном ревью
Применять практику написания validation.md до SQL-кода для ограничения размера решения
Связывать несколько слоёв проверки (спецификация, ODCS, schema.yml, dbt-тесты) в единый валидационный артефакт
Обзор: Проверка данных в Specification-Driven Development — это не абстрактное утверждение «данные корректны», а набор конкретных фактов, которые можно выполнить. Каждый факт должен связывать спецификацию модели, SQL-артефакт и доказательство. Если факт нельзя запустить командой, выполнить SQL-запросом или подтвердить ручным шагом ревьюера, он остаётся пожеланием. Документ validation.md фиксирует эти факты в версионируемом и читаемом виде, превращая выпуск данных из набора SQL-патчей в инженерный процесс. В этом гайде разбираются структура validation.md, минимальный набор фактов, различие между автоматическими и ручными проверками, а также практики написания валидации до написания кода.
Ключевые концепции: Проверочный факт: Утверждение о данных, которое можно выполнить одним из трёх способов: командой (например, dbt test), SQL/singular-тестом или документированным ручным шагом ревьюера. Факт связывает спецификацию, SQL и доказательство. Без этой связки утверждение остаётся пожеланием.
Автоматический факт: Проверочный факт, который запускается командой и завершается кодом выхода или набором строк. Пример: dbt test --select mart_customer_360 ожидает код 0 и отсутствие ошибок. Подходит для свойств, формализуемых в схеме и логике данных: уникальность, not_null, диапазоны, форматы.
Ручной факт: Проверочный факт, который требует чтения изменения или подтверждения от владельца продукта/ревьюера. Необходим там, где решение зависит от контекста контракта: например, допустимо ли добавление нового поля в grain витрины. Ручной факт силён только тогда, когда явно указано, что именно читать и какой вывод сделать.
Зерно (grain) витрины: Определение того, что считается одной строкой в витрине (например, один customer_id = одна строка). Факт о grain обычно автоматический: проверяется уникальность ключа и его not_null. Изменение grain требует ручного ревью.
Защита pii: Проверка того, что в витрину не попали прямые персональные данные (email, телефон, паспорт). Реализуется singular-тестом, который возвращает 0 строк при наличии PII-полей в выходных колонках модели.
Контракт витрины: Список обязательных полей, их типы и правила, зафиксированные в schema.yml и ODCS. Проверочный факт контракта — тесты на обязательность и типы, плюс ручной факт: изменение не меняет поля контракта без явного утверждения.
Lineage и входные модели: Список источников, из которых собирается витрина. Проверочный факт фиксирует, какие модели подаются на вход и не менялись ли источники без обновления спецификации.
Sdd (specification-driven development): Подход к разработке, в котором спецификация пишется до кода. validation.md — один из артефактов SDD, который делает спецификацию проверяемой и устойчивой к изменениям.
Singular-тест dbt: SQL-запрос в файле tests/assert_*.sql, который возвращает строки при нарушении правила. Пустой результат = тест пройден. Используется для сложных проверок, которые нельзя выразить generic-тестами schema.yml.
Слабый факт vs сильный факт: Слабый факт — фраза вроде «проверить качество данных» или «выглядит нормально». Сильный факт содержит команду, ожидаемый результат и статус: «dbt test --select mart_customer_360 возвращает код 0, customer_id unique и not_null, статус: принято».
Важные даты: Концепция validation.md в sdd: Появляется на этапе проектирования витрины (mart) в дата-проекте, до написания dbt-моделей. Файл хранится в specs/validation/ и версионируется в Git вместе со спецификациями и кодом.
Момент пересмотра фактов: При изменении любого из слоёв: спецификации модели, ODCS-контракта, models/schema.yml, dbt-моделей или состава источников. validation.md должен явно показать, какие факты требуют пересмотра.
Практические упражнения: Название: Переписать слабый пункт в три формы
Проблема: В старом чек-листе релиза есть пункт «проверить качество данных в mart_customer_360». Перепишите его в три формы: 1) dbt-команда с ожидаемым результатом, 2) SQL/singular-тест с описанием того, что он должен вернуть, 3) ручной факт ревьюера с указанием, что именно читать и какой вывод сделать.
Решение: Форма 1 (dbt-команда):
### F1 — качество ключа
- Команда: dbt test --profiles-dir . --select mart_customer_360
- Ожидание: код выхода 0; тесты customer_id unique и not_null проходят.
- Статус: принято.
Форма 2 (singular-тест):
-- tests/assert_customer_360_no_orphan_orders.sql
select c.customer_id, c.last_order_id
rom {{ ref('mart_customer_360') }} c
left join {{ ref('dim_orders') }} o on o.order_id = c.last_order_id
where c.last_order_id is not null and o.order_id is null
- Команда: dbt test --profiles-dir . --select assert_customer_360_no_orphan_orders
- Ожидание: тест возвращает 0 строк.
- Статус: принято.
Форма 3 (ручной факт):
### F3 — ревью grain
Ревьюер подтвердил, что добавление product_code в mart_customer_360 изменило бы grain (одна строка на customer × product вместо customer) и не входит в текущую фазу. Решение зафиксировано в тикете DATA-1234.
Сложность: beginner
Название: Собрать минимальный validation.md для витрины
Проблема: Для витрины mart_orders_summary (одна строка = один order_id) создайте минимальный validation.md, который включает: факт про grain, факт про PII, факт про обязательные поля контракта, факт про lineage/входные модели и один ручной факт. Спецификация модели: models/schema.yml содержит order_id (unique, not_null), customer_id (not_null), total_amount (not_null), order_date (not_null). Источники: stg_orders, stg_customers. PII: email, phone — не должны попасть в витрину.
Решение: ```markdown
validation.md — mart_orders_summary
F1 — grain витрины
- Команда: dbt test --profiles-dir . --select mart_orders_summary
- Ожидание: order_id unique и not_null, код выхода 0.
- Статус: принято.
F2 — PII не попало в витрину
- Команда: dbt test --profiles-dir . --select assert_orders_summary_no_pii
- Ожидание: тест возвращает 0 строк (в выходных колонках нет полей email/phone).
- Статус: принято.
F3 — обязательные поля контракта
- Команда: dbt test --profiles-dir . --select mart_orders_summary
- Ожидание: customer_id, total_amount, order_date — все not_null.
- Статус: принято.
F4 — lineage и входные модели
- Команда: dbt list --select +mart_orders_summary --output name
- Ожидание: список содержит stg_orders и stg_customers как прямые предшественники.
- Статус: принято.
F5 — ревью изменения grain
Ревьюер подтвердил, что добавление order_line_id в mart_orders_summary изменило бы grain и не входит в фазу MVP. Решение зафиксировано в тикете DATA-7788.
Сложность: intermediate
Название: Найти и исправить типичную ошибку
Проблема: В проекте коллега написал в validation.md факт: «Данные в витрине корректны и готовы к релизу». Укажите, почему это слабый факт, и перепишите его в сильный. Подсказка: витрина mart_payments, ключ payment_id, источник stg_payments.
Решение: Разбор: фраза «данные корректны» — пожелание, а не факт. Она не содержит команды, не указывает ожидаемый результат и не может быть запущена. Агент может считать корректностью отсутствие ошибок выполнения, аналитик — отсутствие пустых значений, владелец продукта — сохранение grain. Факт убирает эту двусмысленность только если содержит команду, ожидание и статус.
Сильный факт:
F1 — payment_id уникален и не пуст
- Команда: dbt test --profiles-dir . --select mart_payments
- Ожидание: тесты payment_id unique и not_null проходят, код выхода 0.
- Статус: принято.
Дополнительно можно добавить факт про суммы и даты: total_amount ≥ 0, payment_date в пределах последних 90 дней.
Сложность: beginner
Название: Решить, какой факт автоматизировать, а какой оставить ручным
Проблема: Для витрины mart_customer_360 дан список утверждений. Для каждого укажите: автоматический или ручной факт, и обоснуйте выбор. Утверждения: 1) customer_id уникален; 2) в витрине нет PII-полей; 3) добавление product_code не нарушит grain; 4) total_orders рассчитан по актуальным отменам; 5) created_at заполнен у всех строк.
Решение: 1) Автоматический — dbt test на unique и not_null, формализуемо.
2) Автоматический — singular-тест, проверяющий набор колонок модели на наличие PII-имён или значений.
3) Ручной — решение о допустимости нового поля в grain зависит от контракта и фазы, требует чтения спецификации и подтверждения владельца.
4) Автоматический (частично) — singular-тест, который сравнивает total_orders с агрегацией из источника и проверяет, что отмены учтены. Дополнительно ручной факт: ревьюер подтвердил логику обработки отмен в коде модели.
5) Автоматический — generic-тест not_null на колонку created_at.
Сложность: intermediate
Название: Связать слои проверки в одном факте
Проблема: Опишите, как один проверочный факт связывает спецификацию модели (specs/models/mart_customer_360.md), ODCS, models/schema.yml, singular-тест и отчёт ревьюера. Покажите, что произойдёт с фактом, если изменится любой из слоёв.
Решение: Пример факта «customer_id уникален и не пуст»:
- Спецификация фиксирует grain и список обязательных полей.
- ODCS описывает customer_id как обязательное поле с типом string.
- models/schema.yml содержит тесты unique и not_null на customer_id.
- Singular-тест дополнительно проверяет, что customer_id не NULL после всех джойнов (защита от LEFT JOIN с NULL справа).
- Отчёт ревьюера фиксирует, что тесты прошли в CI и статус факта — «принято».
Если меняется спецификация (например, grain становится customer × product), все четыре слоя требуют пересмотра: меняется ODCS, в schema.yml уникальность переносится на составной ключ, singular-тест обновляется, ревьюер перепроверяет факт. validation.md должен явно указать, какие факты пересмотрены.
Сложность: advanced
Кейсы:
Название: Релиз витрины Customer 360: как validation.md спас от PII-утечки
Сценарий: Команда аналитики готовила релиз витрины mart_customer_360 для маркетинга. Витрина собиралась из stg_customers (содержит email и phone) и stg_orders. В код-ревью не попало условие, и поле email случайно осталось в выходном наборе. Спецификация модели явно запрещала прямые PII в витрине.
Задача: Без формальной проверки факт «PII не попало в витрину» существовал только в голове аналитика. Агент считал релиз успешным по коду выхода dbt run, аналитик — по отсутствию пустых значений, владелец продукта — по сохранению grain. Никто не проверил состав колонок.
Решение: Команда внедрила validation.md как обязательный артефакт релиза. Для mart_customer_360 был написан факт F2 с singular-тестом assert_customer_360_no_direct_pii, который сравнивал список колонок модели со списком запрещённых PII-полей. Тест запускался командой dbt test --select assert_customer_360_no_direct_pii и должен был вернуть 0 строк. Факт был включён в чек-лист релиза и блокировал деплой при ошибке.
Результат: При следующей попытке пронести email в витрину тест упал ещё в CI, ревьюер увидел нарушение факта F2 и отклонил merge. PII не утекло в маркетинговую витрину. Процесс релиза стал воспроизводимым: факт можно перезапустить в любой момент и получить тот же ответ.
Извлечённые уроки:
Факт «PII не попало в витрину» без команды и ожидаемого результата — пожелание, а не проверка.
Singular-тест защищает от регрессий, которые не видны в generic-тестах schema.yml.
validation.md должен быть версионируемым артефактом, а не записью в чате.
Связанные концепции:
Защита PII
Singular-тест dbt
Проверочный факт
Ручной и автоматический факт
Название: Изменение grain без валидации: как потеряли месяц отчётности
Сценарий: Аналитик добавил в витрину mart_sales поле product_sku, чтобы дашборд мог показывать продажи по товарам. Изменение прошло код-ревью, тесты not_null и уникальности на order_id остались зелёными. Через две недели отдел финансов обнаружил, что выручка удвоилась: grain витрины фактически стал order_id × product_sku, хотя название и тесты говорили, что grain — order_id.
Задача: В проекте не было validation.md. Тесты проверяли только свойства колонок, но не фиксировали факт «grain витрины — один order_id = одна строка». Никто явно не зафиксировал, что добавление product_sku меняет grain. Ревьюер не получил подсказки, на что обратить внимание.
Решение: После инцидента команда ввела обязательный validation.md для каждой витрины. Для mart_sales появился факт F1 (grain: order_id unique и not_null) и ручной факт F5: «Ревьюер подтвердил, что изменение не меняет поля контракта и grain без явного утверждения владельца». В тикете на добавление product_sku ручной факт требовал бы явного решения: либо менять grain и название витрины, либо вынести product_sku в отдельный слой.
Результат: Через три месяца команда предложила добавить поле channel в витрину. Ревьюер прочитал validation.md, увидел факт F5 и остановил merge, запросив отдельное решение о grain. Время выпуска сократилось, потому что ревью шло по рубрике, а не по интуиции.
Извлечённые уроки:
Уникальность ключа — не то же самое, что зафиксированный grain. Grain — это контракт, его нужно проверять и вручную.
validation.md пишется до SQL: он ограничивает размер решения и подсказывает, какие тесты нужны.
Если validation.md пишется после SQL, он превращается в оправдание уже сделанного, а не в защиту релиза.
Связанные концепции:
Зерно (grain) витрины
Ручной факт
Контракт витрины
SDD
Название: Lineage как проверочный факт: отлов тихого изменения источника
Сценарий: Команда платформы обновила схему в stg_orders, переименовав колонку order_total в order_total_amount. dbt-модели mart_sales продолжали собираться, тесты проходили, потому что в коде использовался ref() и адаптация под новое имя прошла автоматически через переименование переменной. Но витрина mart_sales_weekly сломалась: один из downstream-дашбордов показывал нули, потому что в нём использовался жёстко прописанный SELECT с указанием старого имени.
Задача: Generic-тесты не упали, потому что mart_sales_weekly собирался из промежуточной модели, в которой колонка уже была переименована. Не было факта, фиксирующего состав входных моделей и их схему. Ревьюер не получал сигнала, что источник изменился.
Решение: Команда добавила в validation.md для каждой витрины факт F4 о lineage: команда dbt list --select +<mart> --output name фиксирует список предшественников. Дополнительно введены singular-тесты, которые проверяют наличие обязательных колонок в источнике (information_schema). При переименовании в stg_orders такой тест упал бы на этапе сборки витрины, и ревьюер увидел бы нарушение факта F4.
Результат: Через квартал команда заметила попытку удалить поле customer_segment в stg_customers. Тест на наличие колонки в источнике упал, факт F4 в validation.md был помечен как требующий пересмотра, изменение не прошло в main до явного решения.
Извлечённые уроки:
Lineage — это часть контракта. Изменение состава или схемы источников должно быть видно в validation.md.
Автоматические тесты на information_schema защищают от тихих переименований, которые не ломают generic-тесты.
validation.md связывает несколько слоёв: спецификацию, ODCS, schema.yml, singular-тесты и ревью.
Связанные концепции:
Lineage и входные модели
Singular-тест dbt
Проверочный факт
Контракт витрины
Советы по изучению:
Пишите validation.md до SQL. Сначала сформулируйте факты (grain, PII, контракт, lineage, ручной ревью), потом — тесты и модель. Это сокращает размер решения и делает ревью структурированным.
Держите validation.md в Git рядом со спецификациями и кодом. Файл в чате или в Wiki не может быть доказательством релиза.
Превращайте каждый слабый пункт чек-листа в сильный факт: добавьте команду, ожидаемый результат и статус. Фраза «выглядит нормально» — не факт.
Не пытайтесь автоматизировать всё. Решения, зависящие от контракта (например, что считать активным клиентом), оставляйте ручными фактами с указанием, что именно читать и какой вывод сделать.
Используйте singular-тесты для проверок, которые не выражаются generic-тестами: состав колонок, диапазоны дат, отсутствие PII, ссылочная целостность после джойнов.
При изменении любого слоя (спецификация, ODCS, schema.yml, модель, источник) проходите по validation.md и помечайте факты, требующие пересмотра.
Используйте Qwen (или другой LLM) для черновика validation.md, передавая ему ссылки на specs/models/, ODCS, ODPS и models/schema.yml. Не просите LLM менять dbt-модели — только сгенерировать валидационные факты.
Добавьте validation.md в чек-лист pull request: ревьюер должен прочитать файл до изменения и отметить, какие факты пересмотрены.
Различайте grain (контракт) и уникальность ключа (тест). Уникальность можно проверить автоматически, изменение grain — только вручную с подтверждением владельца.
Минимальный набор фактов для витрины: один про grain, один про PII, один про обязательные поля контракта, один про lineage, один ручной про неизменность контракта без утверждения.
Дополнительные ресурсы:
Dbt documentation — tests: https://docs.getdbt.com/docs/build/tests — официальная документация по generic- и singular-тестам dbt.
Dbt documentation — singular tests: https://docs.getdbt.com/docs/build/singular-tests — руководство по написанию singular-тестов на SQL.
Odcs (open data contract standard): https://bitol-io.github.io/open-data-contract-spec/ — стандарт контракта данных, который связывается с validation.md.
Odps (open data product standard): https://opendataproducts.org/ — стандарт дата-продукта, используемый в SDD как один из слоёв спецификации.
Specification-driven development for data projects: концепция SDD, в которой validation.md — обязательный артефакт релиза, а не опциональный документ.
Dbt-utils package: https://github.com/dbt-labs/dbt-utils — пакет с готовыми тестами (expression_is_true, accepted_range, relationships и др.), которые можно использовать в validation.md.
Резюме: validation.md — это место, где учебник по SDD становится строгим. Проверочный факт отличается от пожелания тем, что его можно выполнить: командой (dbt test), SQL/singular-тестом или документированным ручным шагом ревьюера. Минимальный validation.md для витрины содержит пять фактов: grain, защита PII, обязательные поля контракта, lineage/входные модели и ручной факт о неизменности контракта без утверждения. Не все факты нужно автоматизировать — решения, зависящие от контракта (например, допустимость нового поля в grain), остаются ручными. validation.md связывает несколько слоёв: спецификацию модели, ODCS, models/schema.yml, singular-тесты и отчёт ревьюера. При изменении любого слоя файл должен явно показать, какие факты требуют пересмотра. Главная практика: писать validation.md до SQL — это ограничивает размер решения, подсказывает нужные тесты и даёт ревьюеру готовую рубрику. Если validation.md пишется после SQL, он превращается в оправдание уже сделанного, а не в защиту релиза.