ИНЖЕНЕРНЫЕ МЕТОДЫP0L3

Инкрементальные загрузки и поздние изменения

Контракт снимка и дельты, допуск завершённых поставок, удаление, идемпотентность и пересчёт финансовой истории.

Справочный материал. Контракты данных, архитектурные решения и ограничения применения. Источники приведены в материалах, где используются внешние основания.

Инкремент - это правило изменений, а не только фильтр даты#

В финансовом EDWH поступающие данные могут содержать новые события, исправления существующих записей, сторно, удаление, переиздание документа, отложенную доставку и повторное сообщение. Фильтр WHERE source_timestamp > max(target_timestamp) не решает все эти случаи.

У корректной загрузки должны быть контракт идентичности, политика изменений, определение полноты и граница повторного расчёта.

Стратегии приёма изменений#

Стратегия Когда применима Риск
Append-only Источник гарантирует неизменяемые события Поздняя коррекция требует отдельного события
Upsert / merge Источник передаёт актуальное состояние по стабильному ключу Потеря прежних версий
CDC Доступен журнал изменений источника Неполный журнал или потеря последовательности
Windowed reload Можно перечитывать заданный интервал Изменения вне окна останутся незамеченными
Full reload Объём допускает полную перестройку Высокая стоимость и время

Выбор зависит от семантики источника, а не от любимого инструмента.

Идемпотентность#

Для неизменяемого потока событий повторная обработка одного и того же входа не должна менять логический результат:

load(S, load(S, T)) = load(S, T)

где S - один и тот же набор исходных событий, T - состояние приёмника. Это не означает, что повторная обработка должна игнорировать изменившееся содержимое под прежним ID. Такое противоречие требуется обнаружить и отдельно обработать.

В лаборатории для каждого события вычисляется SHA-256 канонического содержимого. Повтор с тем же ID и содержимым безопасно пропускается; совпадение ID с другой полезной нагрузкой считается ошибкой контракта источника.

Watermark и полнота#

Watermark - ориентир уже обработанного потока, но его значение зависит от выбранного временного поля. Изменение business_date задним числом не всегда меняет timestamp источника, а события могут приходить не по порядку.

Поэтому в надёжной архитектуре возможны сочетания:

  • продвижение watermark после успешной атомарной фиксации загрузки;
  • периодическое перечитывание перекрывающегося окна;
  • CDC или события исправлений;
  • контроль полноты по номеру последовательности/снимку/реестру;
  • целевой пересчёт затронутых финансовых периодов.

Контракт полного снимка и дельты#

Здесь рассматривается поставка состояния финансовых строк источника, а не алгоритм загрузчика. Зерно снимка - одна актуальная строка по ключу (source_system, legal_entity, ledger, document_id, line_id) на согласованной позиции источника. Зерно дельты - одно изменение этой строки с позицией и порядком внутри транзакции. Ключ подходит только источникам, где эти компоненты действительно обеспечивают идентичность. Неизменяемый журнал событий имеет другой контракт: удаление строки состояния нельзя автоматически считать сторно хозяйственной операции.

Для сумм закрепляют валюту, масштаб и смысл знака до преобразования. Например, целые копейки RUB и положительный дебет с отрицательным кредитом допустимы как проектное соглашение, но не как универсальное правило источника. Контрольные суммы считают раздельно по документу и валюте; общий ноль не доказывает полноту поставки.

Поле манифеста Что нужно согласовать
Режим и версия контракта Полный снимок состояния или дельта; допустимые операции и схема
Периметр Юридические лица, книги, таблицы, фильтры, архивные и удалённые записи; версия отбора
Граница Для снимка - согласованная позиция состояния; для дельты - диапазон (start, end] и исходная база
Завершённость Ожидаемый реестр частей, контрольные характеристики и подтверждение источника после завершения выгрузки
Идентичность поставки Идентификатор, ревизия и связь с заменяемой поставкой

business_date определяет финансовый период строки, snapshot_at описывает момент состояния источника, observed_at - получение EDWH. Для упорядочения используют согласованные позиции источника, а не время получения файлов. Часы и позиции разных систем не считаются автоматически сопоставимыми. Если источник не умеет формировать согласованный снимок, это ограничение записывают явно: последовательное чтение таблиц может дать смесь состояний и не допускается как единый согласованный срез.

Завершённость и отсутствие строки#

Отсутствие ключа означает прекращение его присутствия только в полностью принятом снимке того же периметра и при явном соглашении, что снимок перечисляет все актуальные строки. Отсутствие в дельте означает отсутствие переданного изменения, а не удаление. Нельзя вычислять удаления из частично полученных файлов, недоступной таблицы или выгрузки с изменившимся фильтром.

Пустой снимок может быть корректным, но только при независимом подтверждении источника, что этот периметр пуст на закреплённой границе. Нулевой размер файла и успешная доставка не являются таким подтверждением. Получение последнего файла также не доказывает наличие всех предыдущих частей. Реестр частей создают на стороне источника; сверка реестра с поставкой описана в контроле партии событий.

Явное удаление в дельте содержит ключ, операцию, позицию источника и достаточный контекст периметра. Выход строки из фильтра, физическое удаление, архивирование и сторно имеют разный смысл. Если протокол их не различает, EDWH фиксирует только прекращение присутствия в данном наборе, а финансовую трактовку согласует отдельно. Удаление неизвестного ключа сохраняют как наблюдение и оценивают относительно базового снимка, а не молча отбрасывают.

Пересечение поставок и сохранение аудита#

Для снимка на позиции P и дельты (A, B], где A < P < B, сначала принимают состояние P, затем применяют только изменения после P, если контракт позволяет точно отделить их и сохранить границы транзакций. Изменения до P уже отражены в снимке. Если отделение невозможно, поставку блокируют и запрашивают совместимую дельту; время доставки не выбирает победителя. Для независимых потоков нужна отдельная граница каждого потока и подтверждённое правило согласованности, а не один выдуманный глобальный номер.

Поздний старый снимок не заменяет более новое состояние автоматически. Он сохраняется для аудита или допускается как отдельная историческая база. Изменение периметра требует нового базового снимка либо явного переходного контракта: строка вне нового фильтра не становится доказанно удалённой из источника. Повтор поставки и конфликт содержимого рассматриваются отдельно в разделах выше.

До допуска проверяют контракт, периметр, согласованность границы, все части, контрольные результаты и непрерывность относительно принятой базы. Частично принятую дельту нельзя объявлять полным новым состоянием. Исходные байты, манифест, позиции, решение, причины отказа и связь прежней версии с новой сохраняют; обновление текущего состояния не уничтожает историю. Предыдущий финансовый выпуск сохраняет свои входы и не меняется вслед за удалением в новом снимке.

Альтернатива выводу удалений по полному снимку - явный журнал изменений с операциями удаления. Он уменьшает объём повторного чтения, но требует доказуемого перехода от базы и непрерывности потока. Полные снимки проще для источника без такого журнала, но дороже и нуждаются в подтверждённой согласованности. Эти правила - инженерный контракт EDWH, а не требования IFRS или универсальная гарантия CDC.

Конкретная СУБД и критерии приёмки#

PostgreSQL 18 описывает начальное копирование снимка и последующую передачу изменений с сохранением порядка и транзакционной согласованности в рамках одной подписки. Это не гарантия согласованности произвольных файлов или нескольких независимых источников. При фильтрации строк PostgreSQL 18 выход строки из фильтра при UPDATE может передаваться как DELETE. Поэтому транспортная операция удаления сама по себе не доказывает физическое удаление в источнике.

Критерии для будущей реализации: неполный снимок не меняет принятый набор и не порождает удаления; подтверждённый пустой снимок меняет только свой периметр; пустая дельта не удаляет строки; старый снимок не откатывает актуальную базу; пересечение снимка и дельты не применяет изменение дважды; смена фильтра не маскируется под удаление. При отсутствии доказательства границы или завершённости решение - отказ в допуске с сохранённой причиной. Код здесь не исполнялся; SQL-фильтр в начале статьи - иллюстрация опасного подхода, не готовый запрос.

Вопросы для подготовки#

  1. Какие доказательства позволяют отличить пустой полный снимок от потерянной выгрузки?
  2. Почему DELETE из фильтрованного потока не всегда означает удаление хозяйственной операции?
  3. Как принять дельту, пересекающую границу снимка, если источник не позволяет отделить изменения после этой границы?

Сквозной сценарий#

  1. В январе загружен расход 200 RUB.
  2. В феврале поступило январское сторно и замещающий расход 180 RUB.
  3. Журнал событий пополняется двумя новыми событиями, первоначальная запись остаётся.
  4. Январский агрегат пересчитывается по правилам среза времени известности.
  5. Автоматические сверки сравнивают входы, журнал и полученный финансовый результат.

См. временную семантику и исходный код лаборатории.

Источники и документация

  1. dbt - Configure incremental models ↗
  2. PostgreSQL 18 - Logical Replication ↗
  3. PostgreSQL 18 - Row Filters ↗

Источники нужны для самостоятельной проверки. Примеры и интерпретации на сайте могут быть учебными упрощениями.