Тема: Часть 10. Staging-модели dbt
Уровень сложности: Средний
Расчётное время изучения: 2.5–3 часа (теория ~1 ч, практика ~1.5 ч, разбор кейсов ~30 мин)
Предварительные требования: Уверенное владение SQL (SELECT, JOIN, CAST, COALESCE, CASE)
Базовое понимание архитектуры dbt (sources, models, ref, tests)
Знакомство с принципами ELT и слоистой архитектурой витрин (raw → staging → intermediate → marts)
Понятие о Specification-Driven Development (SDD) и Schema Manifest
Минимальный опыт работы с dbt-проектом (dbt run, dbt test)
Цели обучения: Различать допустимые и запрещённые операции в staging-слое dbt и обосновывать границу технической нормализации и бизнес-логики.
Корректно применять приведение типов, обработку пустых значений и null-семантики с учётом фактов источника и спецификации.
Проектировать dbt-тесты для staging-моделей (not_null, unique, accepted_values, singular) и отличать их от проверочных фактов mart-слоя.
Распознавать скрытую бизнес-логику в staging и формулировать требования к её выносу в спецификацию или mart.
Документировать решения по PII, ключам и дрейфу контракта в staging в стиле SDD.
Обзор: Staging-модели в dbt — это первый слой, в котором необработанные данные из источников превращаются в устойчивые технические модели с понятной типизацией, согласованным именованием и явной обработкой null-семантики. Глава формулирует философию staging: «скука здесь полезна». Staging не должен отвечать на продуктовые вопросы, выбирать grain витрины, агрегировать клиентов или скрывать дрейф контракта источника. Его задача — подготовить данные так, чтобы SQL ниже по цепочке был предсказуемым, а ревьюер мог быстро отделить техническую нормализацию от продуктового смысла. В разделе рассматриваются границы разрешённых и запрещённых действий, паттерны приведения типов (включая обработку пустых строк перед CAST к date), базовый набор dbt-тестов, требования к обработке PII, типичные ошибки вроде «прятать бизнес-логику в staging», а также контрольные вопросы и Qwen-промпт, помогающий агенту работать со staging в духе SDD.
Ключевые концепции: Staging как технический слой: Staging — первый слой трансформации, где данные «начинают говорить на языке платформы». Здесь появляются устойчивые имена колонок, явные типы, обработка пустых значений и базовые ключи, но не принимается никаких продуктовых решений. Если модель staging становится «маленькой бизнес-витриной», слой стал слишком умным.
Граница разрешённых действий: В staging допускаются: приведение имён полей к соглашению проекта, приведение типов (date, integer, decimal), явная обработка семантики пустых значений и null, сохранение ключей источника, маркировка PII-полей (но без сокрытия без причины). Эти операции технические, они не отвечают на вопросы потребителя.
Граница запрещённых действий: В staging запрещено: агрегировать клиентов, считать риск-скор, выбирать финальный grain витрины, удалять строки «для красоты», скрывать дрейф контракта источника. Любая такая логика должна быть либо явно записана в спецификации/манифесте, либо вынесена в промежуточный/mart-слой.
Приведение типов и null-семантика: Типичный паттерн: try_cast(nullif(cast(revoked_at as varchar), '') as date) as revoked_at. Эта строка превращает пустое значение в null, но смысл «null в revoked_at = активное согласие» фиксируется в спецификации и учитывается в mart. Техническое преобразование всегда сопровождается продуктовым комментарием.
Dbt-тесты в staging: Минимальный набор: not_null и unique для ключей источника; not_null для обязательных сумм и дат; accepted_values там, где домен мал; singular-тесты для сложных ограничений. Тесты staging ловят технические проблемы источника, но не заменяют проверку дата-продукта.
Sdd и документирование решений: Specification-Driven Development требует: записать, что пустое значение встречается в raw; объяснить, почему в staging оно становится null; проверить, что mart считает активные и отозванные согласия согласованно. Техническое исправление превращается в документированное решение.
Pii в staging: PII-поля остаются видимыми в staging только там, где это нужно для демонстрации политики безопасности. Скрывать их «по умолчанию» — антипаттерн: это маскирует дрейф контракта и мешает верификации.
Qwen-промпт для staging: Канонический промпт агенту: «Прочитай Schema Manifest и raw-источники. Создай или проверь staging-модели. Не добавляй бизнес-агрегации. Для каждого приведения типа объясни факт источника, на котором он основан.» Такой промпт фиксирует границу и снижает риск «умных» моделей.
Стиль sql для агента: Staging-модели должны быть небольшими, читаться сверху вниз, использовать ref() и source() там, где это уместно. Если staging трудно прочитать, модели ниже по цепочке станут ещё хуже.
Типичная ошибка: риск-политика в staging: Прятать бизнес-логику в staging, например: case when amount_rub > 100000 then 1 else 0 end as risk_event допустимо только если правило уже записано в спецификации или манифесте источника. Иначе агент придумывает риск-политику, что нарушает SDD.
Важные даты: 2020: dbt Labs официально закрепили терминологию слоёв staging → intermediate → marts в документации, что упростило стандартизацию архитектур.
2021: Выход dbt-utils и популяризация макросов tests/generic_tests, упростивших написание accepted_values и singular-тестов.
2022: Появление dbt-meta-testing и контрактных тестов, усиливших контроль дрейфа схемы источника именно в staging-слое.
2023: Введение dbt Contracts (версии моделей) — staging стал первой линией защиты от дрейфа контракта источника.
2024: Широкое распространение SDD-практик и Schema Manifest как обязательного артефакта перед генерацией staging-моделей ИИ-агентами.
Практические упражнения: Название: Аудит staging: разделяй техническое и продуктовое
Проблема: Дана staging-модель stg_consents.sql. Тело модели:
select
user_id,
cast(consented_at as date) as consented_at,
try_cast(nullif(cast(revoked_at as varchar), '') as date) as revoked_at,
case when revoked_at is null then 'active' else 'revoked' end as status,
case when amount_rub > 100000 then 1 else 0 end as risk_event,
count(*) over (partition by user_id) as consents_per_user
from {{ source('raw', 'consents') }}
Задание:
- Подпишите каждое преобразование: переименование, приведение типа, обработка null, бизнес-логика.
- Для каждой строки укажите, какие факты источника её обосновывают.
- Найдите бизнес-логику, которая не имеет явной спецификации.
- Перепишите модель так, чтобы в staging остались только технические преобразования, а продуктовая логика была вынесена или помечена как требующая спецификации.
Решение: Шаг 1 — подписываем каждое преобразование:
- cast(consented_at as date) — приведение типа (техническое, факт: поле строковое в raw, домен — дата).
- try_cast(nullif(cast(revoked_at as varchar), '') as date) — обработка null/пустой строки (техническое, факт: в raw встречаются пустые значения, которые ломают cast к date).
- case when revoked_at is null then 'active' else 'revoked' end as status — БИЗНЕС-ЛОГИКА. Семантика 'active' принадлежит спецификации, не staging. Должна быть вынесена в mart или зафиксирована в Schema Manifest.
- case when amount_rub > 100000 then 1 else 0 end as risk_event — БИЗНЕС-ЛОГИКА. Правило 100 000 — это риск-политика, которой нет в staging. Прятать её здесь — типичная ошибка.
- count(*) over (partition by user_id) as consents_per_user — АГРЕГАЦИЯ. Запрещена в staging.
- user_id — сохранение ключа источника (техническое, факт: ключ есть в raw, должен остаться).
Шаг 2 — корректная версия staging:
select
user_id,
cast(consented_at as date) as consented_at,
try_cast(nullif(cast(revoked_at as varchar), '') as date) as revoked_at,
amount_rub
from {{ source('raw', 'consents') }}
Шаг 3 — комментарии в модели или в Schema Manifest:
- revoked_at: «пустое значение в raw → null в staging; семантика active/revoked определяется в mart согласно спецификации S-001».
- amount_rub: «поле сохранено как есть, бизнес-правила (например, risk_event) вычисляются в mart по спецификации».
Вывод: модель стала «скучной» и предсказуемой, бизнес-логика вынесена туда, где ей место.
Сложность: intermediate
Название: Набор dbt-тестов для staging по согласиям
Проблема: Источник — таблица raw.consents с полями: user_id (string), consented_at (string, формат YYYY-MM-DD), revoked_at (string, может быть пустой), source_system (string, домен: 'web', 'mobile', 'partner_api'). Создайте YAML-файл schema.yml с тестами для stg_consents. Обоснуйте, почему именно эти тесты — технические, а не продуктовые.
Решение: ```yaml version: 2
models:
- name: stg_consents
description: "Техническая нормализация согласий из raw.consents. Семантика active/revoked определяется в mart по спецификации S-001." columns:
- name: user_id
description: "Идентификатор пользователя из источника. Ключ staging-модели." tests:
- not_null
- unique
- name: consented_at
description: "Дата выдачи согласия. Источник: строковое поле, приведено к date в staging." tests:
- not_null
- name: revoked_at
description: "Дата отзыва согласия. Пустое значение в raw → null в staging." tests:
- not_null
# Альтернативно: not_null допустим, только если revoked_at по контракту всегда заполнен; # иначе заменяем на singular-тест, проверяющий, что пустые значения превратились в null.
- name: source_system
description: "Источник выдачи согласия." tests:
- accepted_values:
values: ['web', 'mobile', 'partner_api']
Обоснование технического характера тестов:
- not_null + unique на user_id — это проверка целостности ключа источника, а не продуктовая гипотеза.
- not_null на consented_at — фиксирует контракт, что дата выдачи всегда присутствует.
- accepted_values на source_system — ловит появление нового канала, который не прошёл онбординг, и сигнализирует о дрейфе контракта.
- Продуктовые тесты (например, «нет пользователя с revoked_at позже consented_at», «нет двух активных согласий одного типа») пишутся в mart или в tests/ папке с singular-тестами для дата-продукта.
Сложность: intermediate
Название: Обработка пустого revoked_at: от факта источника к документированному решению
Проблема: В raw.consents поле revoked_at приходит как строковое и в 30% строк содержит пустую строку. Текущий код: cast(revoked_at as date) as revoked_at. При пустой строке dbt отдаёт ошибку или возвращает неожиданные значения. Перепишите преобразование и подготовьте комментарий для Schema Manifest.
Решение: Корректное преобразование:
try_cast(nullif(cast(revoked_at as varchar), '') as date) as revoked_at
Логика:
1. cast(revoked_at as varchar) — нормализуем тип (на случай, если в источнике он приходит иным).
2. nullif(..., '') — превращаем пустую строку в null. Это технический шаг, не продуктовое решение.
3. try_cast(... as date) — безопасно приводим к date; если строка не парсится, получаем null.
Запись в Schema Manifest (фрагмент):
fields:
- name: revoked_at
raw_type: string raw_semantics: "Дата отзыва согласия. В raw встречаются пустые строки (~30% строк), которые означают «согласие не отозвано»." staging_type: date staging_semantics: "Пустая строка из raw приведена к null. Семантика «null = активное согласие» закреплена в спецификации S-001 и реализована в mart." mart_usage: "mart.consent_status вычисляет active/revoked по правилу: revoked_at is null → active, иначе → revoked."
Вывод: техническое исправление (try_cast + nullif) становится частью документированного решения, которое согласуется между staging, спецификацией и mart.
Сложность: intermediate
Кейсы:
Название: Кейс финтех-стартапа: «риск-скор в staging сломал регуляторный отчёт»
Сценарий: Команда аналитики финтех-стартапа внедрила ИИ-агента, который генерировал staging-модели на основе Schema Manifest. Агент создал stg_transactions, в которой помимо технических преобразований появилась колонка risk_event: case when amount_rub > 100000 then 1 else 0 end. Правило порога 100 000 не было зафиксировано в спецификации — агент «придумал» его сам, опираясь на общие практики AML-комплаенса.
Задача: Через три месяца регулятор запросил отчёт о подозрительных операциях. Команда собрала витрину mart.suspicious_transactions на основе risk_event из staging и отправила в регулятор. Внутренний аудит обнаружил, что: (1) правило 100 000 нигде не задокументировано; (2) другая команда (compliance) параллельно использовала порог 250 000 согласно свежей версии политики; (3) mart-витрина отличалась от регуляторного отчёта compliance на 12% строк. Потребовалось срочно пересмотреть архитектуру и обосновать регулятору источник правила.
Решение: Команда провела ретроспективу и предприняла следующие шаги:
1. Удалила risk_event из stg_transactions, оставив только технические преобразования: приведение типов, обработку null, переименование полей.
2. Описала правило risk_event в Schema Manifest с указанием источника (compliance policy v2.3, порог 250 000) и ссылки на регуляторный документ.
3. Перенесла логику в mart.suspicious_transactions, где правило стало явным, версионируемым и подписываемым владельцем продукта.
4. Добавила dbt-тест в mart: singular-тест, проверяющий, что все строки с risk_event = 1 имеют amount_rub >= 250000 и поле policy_version = 'v2.3'.
5. В промпт агенту добавила явное ограничение: «Не добавляй бизнес-агрегации и пороги в staging. Если видишь, что правило не зафиксировано в Schema Manifest, пометь это как TODO и не генерируй колонку».
Результат: Регуляторный отчёт был пересобран за 2 дня, расхождение с compliance устранено. Команда формализовала правило: staging принимает только решения, перечисленные в главе (имена, типы, null, ключи, маркировка PII). Бизнес-логика допускается в staging только при наличии ссылки на спецификацию. Через квартал похожая ситуация с порогом в 1 млн была отловлена агентом на этапе генерации — он выдал TODO вместо колонки, и команда вовремя обновила Schema Manifest.
Извлечённые уроки:
Staging — не место для порогов и бизнес-классификаций: даже «безобидный» case when порождает регуляторные риски.
Любое числовое правило в staging должно иметь ссылку на спецификацию или манифест источника; иначе это придуманная агентом политика.
Промпт агенту должен явно запрещать бизнес-агрегации и требовать ссылку на спецификацию для любой нестандартной колонки.
Документирование null-семантики и порогов в Schema Manifest делает аудит и пересборку отчётов дешёвыми.
Связанные концепции:
Граница разрешённых/запрещённых действий в staging
SDD и Schema Manifest
Типичная ошибка: риск-политика в staging
Qwen-промпт для staging
Различие staging-тестов и проверочных фактов mart
Название: Кейс e-commerce платформы: «PII скрыли в staging, и сломали отладку»
Сценарий: Data-инженер e-commerce платформы решил «обезопасить» данные ещё в staging и замаскировал PII-поля (email, phone) хешами прямо в stg_customers. Это было сделано «на всякий случай», без согласования с security-командой и без записи в Schema Manifest. Через полгода команда fraud-аналитики не смогла воспроизвести инцидент: для расследования нужен был исходный email, но в staging уже хранился только хеш, а источник raw.customers был ротирован и недоступен.
Задача: Fraud-команда не смогла провести расследование заявленного инцидента, регулятор (в части антифрод-отчётности) запросил подтверждение, что конкретный email был замечен в подозрительной транзакции. Восстановить связь «email → пользователь → транзакция» стало невозможно без повторной выгрузки из источника, которая требовала отдельного согласования и занимала 5 рабочих дней.
Решение: Команда провела аудит и зафиксировала новые правила:
1. PII в staging остаётся видимым по умолчанию; маскирование выполняется только в mart-слое для конкретных потребителей (например, для маркетинговой витрины).
2. В Schema Manifest для каждого PII-поля указывается уровень чувствительности (low/medium/high) и ссылка на политику безопасности.
3. В stg_customers колонки email и phone возвращены в исходном виде, а маскирование реализовано в mart.marketing_customers как view с применением макроса mask_pii().
4. Для fraud-команды создан отдельный mart.fraud_investigation, где PII сохраняется в открытом виде, доступ регулируется на уровне грантов warehouse, а не на уровне трансформации.
Результат: Расследование инцидента завершилось за 1 день после выкатки изменений. Security-команда получила явный реестр PII-полей в Schema Manifest и смогла выстроить политику доступа по слоям, а не по «интуиции» инженеров. Через квартал аналогичная история с телефоном была предотвращена: junior-аналитик попросил агента «замаскировать phone в staging» — агент вернул отказ со ссылкой на политику и предложил создать отдельный mart.
Извлечённые уроки:
PII в staging должен оставаться видимым: маскирование — это продуктовое решение, оно принадлежит mart-слою.
Скрытие PII в staging маскирует дрейф контракта источника: если поле исчезнет или изменит формат, вы заметите это слишком поздно.
Политика доступа к PII должна регулироваться на уровне грантов и ролей, а не на уровне трансформации в staging.
Schema Manifest — обязательное место для фиксации того, какие PII-поля присутствуют в источнике и где они маскируются.
Связанные концепции:
PII в staging
Schema Manifest
Граница разрешённых действий (маркировка, но не сокрытие)
Документирование решений в SDD
Советы по изучению:
Перед написанием staging-модели откройте Schema Manifest и подчеркните поля, для которых нет спецификации. Если их больше двух — остановитесь и запросите спецификацию у владельца продукта.
Правило «скучного staging»: если модель приятно читать сверху вниз без ветвлений и CASE — скорее всего, она в правильном слое. Если вы ловите себя на мысли «тут нужен комментарий, чтобы объяснить логику» — логика не принадлежит staging.
Всегда применяйте паттерн try_cast(nullif(cast(... as varchar), '') as date) для полей, которые в raw могут приходить пустыми. Это убирает 90% ошибок приведения типов.
Перед коммитом staging-модели пройдитесь по каждой строке и подпишите её: «переименование», «приведение типа», «обработка null», «бизнес-логика». Если на последнюю категорию нет ссылки на спецификацию — удалите строку.
Для Qwen/ИИ-агента используйте канонический промпт из главы как шаблон и добавляйте явные запреты: «не добавляй case when с порогами», «не считай оконные функции, кроме технических row_number».
dbt-тесты в staging должны быть техническими: not_null, unique, accepted_values. Продуктовые тесты (например, «нет двух активных согласий одного типа») — в mart.
Если PII нужно скрыть — создайте отдельный mart, а не трогайте staging. Staging — место, где дрейф контракта должен быть виден.
Периодически (раз в квартал) проводите аудит staging: ищите строки с бизнес-логикой, не имеющей ссылки на спецификацию. Это самый надёжный способ не дать слою «поумнеть».
Сравнивайте staging и mart по принципу «staging отвечает на вопрос ‘что пришло из источника’, а mart — на вопрос ‘что это значит для бизнеса’». Если staging начинает отвечать на второй вопрос — он не в том слое.
Дополнительные ресурсы:
Dbt documentation — staging models: https://docs.getdbt.com/best-practices/how-we-structure/1-staging — каноническое описание staging-слоя от dbt Labs с примерами и антипаттернами.
Dbt-utils (тесты и макросы): https://github.com/dbt-labs/dbt-utils — набор макросов для not_null, unique, accepted_values, expression_is_true и singular-тестов.
Dbt contracts (версионирование моделей): https://docs.getdbt.com/docs/collaborate/govern/model-contracts — документация по контрактам моделей, усиливающим защиту от дрейфа в staging.
Книга «analytics engineering with dbt»: https://www.amazon.com/Analytics-Engineering-dbt-Airflow-BigQuery/dp/1098142370 — глава про layering и границу staging/marts.
How we structure our dbt projects (discourse): https://discourse.getdbt.com/t/how-we-structure-our-dbt-projects/355 — классическая статья команды dbt Labs о структуре слоёв.
Specification-driven development (sdd): https://martinfowler.com/articles/replaceThrowWithException.html — общие принципы SDD, адаптированные к аналитике в статьях dbt-сообщества.
Qwen documentation (промпт-инжиниринг): https://qwen.readthedocs.io/en/latest/ — документация Qwen для составления эффективных промптов аналитическим агентам.
Dbt-project-evaluator: https://github.com/dbt-labs/dbt-project-evaluator — автоматические тесты структуры проекта, которые ловят staging-модели с неожиданной логикой.
Резюме: Staging-модели в dbt — это технический слой, который превращает raw-данные в устойчивые, типизированные, документированные модели, не принимая продуктовых решений. В staging разрешены: приведение имён и типов, явная обработка null и пустых значений, сохранение ключей источника, маркировка PII. В staging запрещены: агрегации, бизнес-фильтры, выбор grain витрины, расчёт риск-скора, удаление строк «для красоты», сокрытие дрейфа контракта. Ключевой паттерн — try_cast(nullif(cast(... as varchar), '') as date) — безопасно превращает пустые строки в null, а смысл «null = активное согласие» фиксируется в спецификации и реализуется в mart. dbt-тесты в staging носят технический характер (not_null, unique, accepted_values, singular для сложных ограничений) и ловят проблемы источника, но не заменяют проверку дата-продукта. Главная ловушка — прятать бизнес-логику в staging: case when amount_rub > 100000 then 1 else 0 end as risk_event допустим только при наличии ссылки на спецификацию, иначе агент придумывает политику. PII в staging остаётся видимым; маскирование — задача mart. Хороший staging «скучный»: небольшой, читается сверху вниз, использует ref()/source(), и его легко ревьюить. Если staging становится сложным, модели ниже по цепочке становятся ещё хуже; если staging понятен — ревьюер быстро отделяет техническую нормализацию от продуктового смысла. После освоения главы у вас должны быть: staging-модели для основных источников, ключи покрыты dbt-тестами, решения по null записаны в Schema Manifest, PII остаётся видимым там, где это нужно для демонстрации политики.