Тема: Часть 13. Инкременты, snapshots и история
Уровень сложности: Средний
Расчётное время изучения: 2-3 часа
Предварительные требования: Базовое понимание dbt (data build tool) и моделей данных
Знание SQL на уровне написания запросов и понимания оконных функций
Понимание концепций хранилищ данных и ETL/ELT процессов
Знакомство с предметной областью банковской отчётности (клиенты, платежи, кредиты, согласия)
Базовое понимание концепции воспроизводимости данных
Цели обучения: Различать три режима истории данных (full refresh, incremental, snapshot) и понимать, когда какой применять
Формулировать корректные спецификации для моделей с историей, включая ключ, дату события и политику переигрывания
Определять набор проверочных фактов, доказывающих безопасность переигрывания модели
Анализировать требования к поздно пришедшим данным и формулировать контрактные ограничения
Применять полученные знания для выбора стратегии истории в банковском контуре (клиенты, карты, кредиты, согласия)
Обзор: История данных — это не просто техническая деталь, а контракт о времени между источником и потребителем. В банковской отчётности неспособность восстановить вчерашнее состояние таблицы делает аудит невозможным и подрывает доверие к моделям. Глава вводит три режима истории (full refresh, incremental, snapshot) как разные «обещания о времени», показывает, что должна содержать корректная спецификация модели с историей, описывает минимальные проверочные факты и политику переигрывания. Особое внимание уделяется поздно пришедшим данным, которые меняют воспроизводимость результата и потому должны описываться до написания SQL. Читатель научится видеть, где история понадобится в боевом контуре, и формулировать явные решения вместо того, чтобы оставлять их «на усмотрение агента».
Ключевые концепции: Full refresh: Режим, при котором таблица целиком пересобирается из источников при каждом запуске. Даёт простую, всегда свежую картину «на сегодня», но не сохраняет вчерашнее состояние. Подходит для учебных примеров, маленьких таблиц и нечасто меняющихся справочников. В боевом контуре становится дорогим и небезопасным, если таблица большая и зависит от других моделей.
Incremental model: Режим, при котором dbt добавляет или обновляет только изменившиеся строки по уникальному ключу и окну дат. Требует явной стратегии (insert, merge, delete+insert), уникального ключа и политики переигрывания. Экономит ресурсы, но создаёт скрытые обязательства: что делать с поздними строками, как долго хранить окно, можно ли пересчитать прошлое.
Snapshot: Режим, который хранит изменение атрибута во времени. Каждая версия строки получает временную метку действия (dbt_valid_from, dbt_valid_to), что позволяет ответить на вопрос «что мы знали вчера». Подходит для статусов, сегментов, согласий и медленно меняющихся измерений, где важна аудит-трассируемость.
Уникальный ключ (unique key): Атрибут или набор атрибутов, однозначно идентифицирующих строку в модели. Без явного ключа невозможно ни обновление, ни корректный snapshot, ни проверка дубликатов. Ключ должен быть стабильным во времени и не зависеть от изменяемых бизнес-полей.
Дата события (event date): Поле, определяющее момент времени, к которому относится факт. Отличается от даты загрузки строки: операция может произойти вчера, а прийти в систему сегодня. Дата события — основа для окон переигрывания, фильтров инкрементальной модели и SCD-логики snapshot.
Политика переигрывания (replay policy): Явное описание того, можно ли пересчитать прошлое и за какой период. Без неё любой перезапуск модели может незаметно изменить исторические агрегаты. Политика отвечает: какие строки разрешено обновлять задним числом, какие требуют PatchSpec и подтверждения ревьюера.
Поздно пришедшие данные (late-arriving data): Строки, чья дата события старше последней даты в инкрементальной модели. Их обработка — главный источник скрытого смыслового дрейфа. Решения (принимать ли их в агрегаты, за какое окно, что делать с уже построенными отчётами) нельзя оставлять внутри is_incremental() — это контрактная история.
Patchspec: Формальная заявка на изменение уже зафиксированного исторического факта. Используется, когда поздняя корректировка затрагивает период, выходящий за рамки окна переигрывания, и требует подтверждения ревьюера и явного описания последствий.
Проверочные факты (validation facts): Минимальный набор утверждений, доказывающих корректность истории модели: повторный dbt build не меняет результат без изменения raw, уникальный ключ не дублируется, дата события не null, окно переигрывания описано, ревьюер вручную подтвердил семантику истории.
Воспроизводимость (reproducibility): Свойство модели, при котором её результат можно восстановить по фиксированному набору входных данных и спецификации. Уничтожается любой неявной перезаписью прошлого, включая full refresh таблицы, для которой требуется история.
Важные даты: Соглашение базель iii (2010-2011): Ужесточение требований к воспроизводимости банковской отчётности, включая риск-отчётность и сегментацию клиентов; повышает значимость snapshot-подходов
Вступление в силу gdpr (25 мая 2018): Усилило требования к фиксации и аудиту согласий клиентов, сделав историю согласий обязательной, а не желательной
Стандарт ifrs 9 (введён поэтапно с 2018): Требует отслеживания стадий кредитного риска (Stage 1/2/3) и просрочки во времени, что невозможно без явной истории
Требования 152-фз (действует с 2014): Российский закон о персональных данных, обязывающий хранить информацию об обработке и согласиях в исторической перспективе
Практические упражнения: Название: Выбор стратегии истории для источника
Проблема: Вам дана таблица raw.transactions с колонками transaction_id, client_id, transaction_date, amount, status, load_dttm. За 30 дней в таблице около 5 миллионов строк, средний объём ежедневных новых операций — 150 000. Статус операции может меняться задним числом (например, с PENDING на CONFIRMED или DECLINED) в течение 14 дней с даты операции. Определите подходящую стратегию истории (full refresh, incremental или snapshot), запишите уникальный ключ, дату события, политику переигрывания и правила обработки поздно пришедших данных.
Решение: 1. Выбираем incremental model, потому что full refresh на 5 млн строк слишком дорог, а snapshot избыточен — нам нужны текущие статусы операций, а не история их смены. 2. Уникальный ключ — transaction_id (стабильный, не зависит от статуса). 3. Дата события — transaction_date, а не load_dttm. 4. Политика переигрывания: разрешено обновлять строки, у которых transaction_date попадает в последние 14 дней от текущей даты; для более старых строк требуется PatchSpec. 5. Поздно пришедшие данные: если load_dttm сегодняшний, а transaction_date старше 14 дней — строка принимается, но агрегаты за этот день не пересчитываются без ревьюера. 6. Стратегия инкремента: merge по transaction_id, фильтр в is_incremental() по transaction_date >= current_date - 14. 7. Проверочные факты: повторный dbt build не меняет итоговую таблицу без новых raw, уникальность transaction_id соблюдена, transaction_date is not null.
Сложность: intermediate
Название: Составление спецификации для модели с историей
Проблема: Перепишите плохую формулировку спецификации в хорошую. Плохая формулировка: «Сделать инкрементальную загрузку для модели client_segments». Дополнительный контекст: модель хранит сегмент клиента (MASSE, VIP, STANDARD, RISK), сегмент пересматривается ежеквартально, но может меняться чаще при крупных событиях (выход в дефолт, жалоба регулятору).
Решение: Плохая формулировка: «Сделать инкрементальную загрузку». Хорошая формулировка: «Модель client_segments использует client_id как уникальный ключ и segment_effective_date как дату события. Сегмент клиента определяется на каждую дату segment_effective_date. Изменения внутри квартала (выход в дефолт, жалоба регулятору) могут обновлять запись клиента задним числом, но только в пределах текущего квартала. Изменения за прошлые кварталы требуют PatchSpec и подтверждения ревьюера. Для аудита сегмента во времени использовать snapshot client_segments_history с полями dbt_valid_from и dbt_valid_to, где ключ snapshot — client_id, стратегия — timestamp. Семантика истории подтверждена руководителем риск-отчётности до первого релиза».
Сложность: intermediate
Название: Диагностика проблемы с историей
Проблема: Команда риск-отчётности жалуется: «Вчера в дашборде клиент был в сегменте VIP, сегодня его нет». Модель client_segments работает в режиме full refresh. Что произошло? Как должна была быть устроена модель?
Решение: Что произошло: full refresh пересобрал таблицу, и текущий сегмент клиента изменился (например, клиент перестал соответствовать критериям VIP). История предыдущего состояния не сохранилась. Дашборд показывает только текущий снимок и не может ответить на вопрос «что мы знали вчера». Решение: модель client_segments должна была иметь либо snapshot (с полями dbt_valid_from, dbt_valid_to и ключом client_id), либо инкрементальный режим с полной историей изменений. Для аналитических витрин сегментов правильный выбор — snapshot, потому что сегмент меняется редко, но каждое изменение значимо для аудита. Дашборд должен обращаться к витрине с фильтром по dbt_valid_from <= report_date < dbt_valid_to, а не к текущей таблице. Дополнительно нужно описать в спецификации политику переигрывания: возможно ли задним числом менять сегмент за прошлые периоды, или это требует PatchSpec.
Сложность: intermediate
Название: Qwen-запрос для аудита моделей
Проблема: Сформулируйте запрос к ассистенту Qwen, который найдёт в вашем dbt-проекте все модели, где требуется история, но не описана. Сформулируйте, что должен вернуть ассистент для каждой такой модели, чтобы результат можно было превратить в задачи.
Решение: Текст запроса: «Прочитай specs и marts. Найди модели, где нужна история, incremental strategy или snapshot. Для каждой верни ключ, дату события, политику переигрывания и открытые вопросы. SQL не меняй». Ассистент должен вернуть таблицу вида: model_name | unique_key | event_date | history_strategy | replay_window | late_data_policy | open_questions. Например, для client_segments: client_id | segment_effective_date | snapshot | N/A (текущий квартал) | изменения в текущем квартале — да, ранее — только через PatchSpec | «не описана стратегия snapshot», «нет проверочного факта о воспроизводимости». На основе этого ответа формируется бэклог задач: добавить спецификацию, реализовать snapshot, написать тесты на дубликаты ключа и непустую дату события, согласовать политику с владельцем модели.
Сложность: beginner
Кейсы: Название: Потеря истории согласий клиента в Open API
Сценарий: Крупный российский банк внедрил Open API для партнёрских сервисов (страховые, брокеры, маркетплейсы). Каждый запрос партнёра требует активного согласия клиента на передачу данных. Согласия хранятся в модели consents, которая работает в режиме full refresh: каждый запуск полностью пересобирает таблицу, и в ней остаётся только последний статус (active или revoked).
Задача: В июне 2024 года клиент пожаловался в ЦБ, что партнёр-брокер передал его данные страховой без разрешения. Банк попытался восстановить историю: «какое согласие было активно 15 мая, когда партнёр отправил запрос?» Выяснилось, что таблица consents за 15 мая уже пересобрана — текущая версия показывает только статус на сегодня. Журнал аудита есть, но он не связан с моделью через уникальный ключ и дату события. Регулятор запросил восстановление истории за 6 месяцев, а данных нет. Параллельно риск-отдел обнаружил, что часть операций в марте имела статус «согласие активно», хотя в действительности клиент отозвал его в феврале — full refresh заменил «revoked» на «active» за все прошлые периоды. Воспроизводимость отчётности нарушена, аудит невозможен.
Решение: Команда данных в течение 6 недель реализовала следующие изменения. 1) Создана snapshot-модель consents_history с уникальным ключом (client_id, partner_id, scope) и стратегией timestamp по полю updated_at; в таблице теперь хранятся пары dbt_valid_from / dbt_valid_to. 2) Текущая витрина consents осталась full refresh, но снабжена явной отметкой «не использовать для аудита прошлого». 3) Написана спецификация для каждой модели с историей: ключ, дата события (consent_updated_at), политика переигрывания (изменения активны задним числом в пределах 30 дней, далее — только через PatchSpec с подтверждением комплаенса), правила обработки поздно пришедших данных (если updated_at < 30 дней — принять; если старше — записать в исключения, агрегаты не пересчитывать). 4) Внедрён тест на уникальность ключа в snapshot и тест «повторный dbt build не меняет итог без новых raw». 5) Восстановление истории за 6 месяцев выполнено по журналу аудита с ручной сверкой по 200 случайным клиентам; точность восстановления — 99,4% (расхождения в 0,6% случаев объяснены и задокументированы).
Результат: Банк предоставил регулятору восстановленную историю согласий и подробный отчёт об архитектуре хранения. Штрафные санкции удалось смягчить за счёт прозрачности и оперативности реакции. После инцидента команда зафиксировала регламент: «ни одна модель, содержащая статус или сегмент клиента, не релизится без явной политики истории и проверочных фактов». В течение следующего квартала аналогичная схема применена к моделям client_segments, credit_risk_stages, card_status — везде, где ранее использовался full refresh с потерей истории. Среднее время восстановления истории по запросу регулятора сократилось с 3 недель до 2 дней.
Извлечённые уроки: Режим materialized='incremental' сам по себе не решает историю — он только меняет способ записи. Смысл истории задают ключи, даты события и политика переигрывания
Full refresh допустим только для моделей, где прошлое не имеет значения; для всего, что касается статусов, согласий и сегментов, нужен snapshot или инкрементальная модель с явной историей
Поздно пришедшие данные — это контрактная, а не техническая проблема: нельзя оставлять их обработку «на усмотрение is_incremental()», нужно явно описать окно и исключения
Воспроизводимость отчётности — это свойство системы в целом; даже если отдельные таблицы корректны, потеря связи между ними через время делает аудит невозможным
Спецификация модели с историей должна отвечать на 5 вопросов: ключ, дата события, возможность пересчёта, политика поздних данных, команда доказательства безопасности переигрывания
Связанные концепции: Snapshot
Incremental model
Поздно пришедшие данные
Политика переигрывания
Проверочные факты
Спецификация модели с историей
Название: Поздно пришедший платёж и сломанные агрегаты
Сценарий: Розничный банк считает ежедневные агрегаты по карточным операциям: объём трат, средний чек, доля отказов. Агрегаты строятся инкрементальной моделью daily_card_aggr, которая в 02:00 берёт все операции за вчера и обновляет соответствующий агрегат. Уникальный ключ — operation_id, дата события — operation_date, окно переигрывания — 3 дня.
Задача: Утром 10 июня служба фрод-мониторинга обнаружила, что в отчёте за 5 июня пропущена крупная операция (180 000 ₽), которая прошла только 9 июня. Причина — в ночь с 5 на 6 июня процессинг вернул задержанную операцию, и dbt-сборка 6 июня уже не пересчитывала агрегат за 5 июня (за пределами окна в 3 дня). Служба фрод-мониторинга приняла решение на основе неполных данных. Параллельно аналогичная ситуация возникла с операцией 7 июня, которая пришла 10-го: агрегат за 7-е был обновлён, а за 8-е и 9-е остались без соседних контекстных операций. Команда не могла объяснить, «правильны» ли агрегаты за прошлую неделю в принципе.
Решение: Команда пересмотрела политику переигрывания. 1) Окно переигрывания увеличено с 3 до 7 дней для крупных операций (amount > 100 000 ₽), и в dbt-модель добавлена ветка: «if amount > threshold, расширить окно до 7 дней». 2) Для всех поздно пришедших операций вне окна создан отдельный стейджинговый слой late_operations, который не правит агрегаты, а попадает в отчёт об исключениях. 3) Спецификация модели дополнена пунктом: «операции с датой события старше 7 дней могут быть включены в агрегат только через PatchSpec с указанием причины и согласованием с аналитиком». 4) Внедрён ежедневный тест: «сумма операций в raw за последние 7 дней равна сумме операций в агрегате за те же 7 дней», который ловит расхождения до того, как их заметят потребители. 5) Службе фрод-мониторинга предоставлен доступ к late_operations с приоритетом.
Результат: За следующие 3 месяца зафиксировано 47 случаев поздно пришедших операций старше 7 дней. Все случаи обработаны по регламенту: 38 приняты в агрегаты через PatchSpec, 9 признаны ошибками процессинга и отклонены. Служба фрод-мониторинга перестала жаловаться на «потерянные» операции и начала доверять отчётам. Средний срок реагирования на крупную аномалию сократился с 24 часов до 6 часов. Команда сформулировала принцип: «окно переигрывания должно быть согласовано с максимальным разумным сроком задержки данных в источнике, а не с удобством dbt-сборки».
Извлечённые уроки: Размер окна переигрывания — это контрактное решение, а не технический параметр; он должен отражать реальные задержки в источниках, а не желание сэкономить время на сборке
Поздно пришедшие данные нужно не «молча отбрасывать», а явно фиксировать в отдельном слое и доводить до владельцев отчётности
PatchSpec — это не «костыль для ошибок», а нормальный механизм внесения задним числом изменений с явной ответственностью ревьюера
Проверочные факты должны включать сверки между raw и marts, а не только тесты на уникальность и непустые значения
Семантика истории (можно ли менять агрегат за прошлый день) важнее способа записи (merge, insert, snapshot)
Связанные концепции: Incremental model
Поздно пришедшие данные
Политика переигрывания
PatchSpec
Проверочные факты
Советы по изучению: Начните с таблицы трёх режимов истории (full refresh, incremental, snapshot) и для каждого режима сформулируйте «обещание о времени» одной фразой — это поможет быстро выбирать стратегию на практике
Для каждой модели в своём проекте попробуйте ответить на 5 вопросов спецификации (ключ, дата события, можно ли пересчитать прошлое, что делать с поздними данными, какая команда доказывает безопасность переигрывания) — если хотя бы на один вопрос ответить не можете, история не описана
Не путайте дату события и дату загрузки (load_dttm): это две разные величины, и инкрементальная модель должна фильтровать по дате события, иначе вы потеряете поздно пришедшие данные
Сформулируйте запрос к Qwen (или любому LLM-ассистенту) по своему проекту по шаблону из главы — это даст быстрый срез моделей, где история не описана, и превратит абстрактную тему в конкретный бэклог
Заведите в проекте отдельный документ «Политика истории» по шаблону из главы (Атрибуты клиентов, Карточные операции, Кредиты, Open API consents, Переигрывание и поздние данные) и обновляйте его при каждом релизе новой модели со статусом или сегментом
Проверочные факты пишите как исполняемые проверки, а не как абзац текста: тест на уникальность ключа, тест «повторный dbt build == dbt build», сверка сумм raw и marts по окну
При выборе между incremental и snapshot думайте не о размере таблицы, а о вопросах потребителей: если спрашивают «сколько сейчас» — incremental, если «что было вчера» — snapshot
Типичная ловушка: установить materialized='incremental' и считать, что история решена. Всегда задавайте вопрос «что произойдёт, если в raw придёт обновление строки за прошлый месяц?» — если ответ не описан, история не решена
Учитесь читать спецификации критически: формулировка «сделать инкрементальную загрузку» — это красный флаг, требующий уточнения всех 5 пунктов
Для подготовки к контрольным вопросам пройдитесь по каждому из них с реальным примером из вашего проекта: incremental vs snapshot, факты безопасности переигрывания, контракт vs SQL для поздних данных
Дополнительные ресурсы: Документация dbt по incremental моделям: https://docs.getdbt.com/docs/build/incremental-models — официальное руководство по конфигурации is_incremental(), стратегиям merge/insert/delete+insert, работе с уникальным ключом и окнами дат
Документация dbt по snapshots: https://docs.getdbt.com/docs/build/snapshots — руководство по SCD Type 2, стратегиям timestamp и check, полям dbt_valid_from / dbt_valid_to, графу зависимостей snapshots
Книга 'fundamentals of data engineering' (joe reis, matt housley): Глава про постепенную загрузку и CDC, включая паттерны для поздно пришедших данных и окон переигрывания
Книга 'data quality: the accuracy dimension' (jack olson): Главы про воспроизводимость и аудит данных, применимые к банковской отчётности
Стандарт ifrs 9 (мсфо 9): https://www.ifrs.org — официальный текст стандарта, разъясняющий требования к истории стадий кредитного риска и просрочки
Соглашение базель iii: Документы Базельского комитета по банковскому надзору (BCBS) — требования к воспроизводимости риск-отчётности
Dbt-utils пакет: https://github.com/dbt-labs/dbt-utils — готовые тесты и макросы, включая surrogate_key, проверку непустых значений и сверки между моделями
Блог dbt labs 'how we structure our dbt projects': Раздел про слои staging / intermediate / marts и описание контрактов между слоями, включая историю
Резюме: История данных — это контракт о времени, а не техническая деталь реализации. Глава вводит три режима (full refresh, incremental, snapshot) как разные «обещания о времени»: пересобрать текущее, добавить/обновить изменившееся, сохранить изменение атрибута. Выбор между ними должен быть записан в спецификации до SQL: какой ключ определяет строку, какое поле — дату события, можно ли пересчитать прошлое, что делать с поздно пришедшими данными, какая команда доказывает безопасность переигрывания. Поздно пришедшие данные — главный источник скрытого смыслового дрейфа, и их обработка относится к контракту, а не к is_incremental(). В банковском контуре история обязательна для атрибутов и сегментов клиентов, карточных операций, стадий кредита и согласий Open API. Минимальные проверочные факты (повторный dbt build не меняет результат, уникальность ключа, непустая дата события, описанное окно, подтверждение ревьюера) превращают историю из намерения в измеримое свойство модели. Главный практический вывод: документ «Политика истории» должен существовать в каждом проекте и явно фиксировать стратегию для каждой сущности, даже если первая версия остаётся простым full refresh.