Сверка партии событий: полнота, повторы и конфликты
Как закрепить ожидаемый набор партии, различить повтор доставки и конфликт содержимого и получить воспроизводимый протокол сверки.
8 мин чтения·База знаний
Справочный материал. Контракты данных, архитектурные решения и ограничения применения. Источники приведены в материалах, где используются внешние основания.
Перед допуском партии финансовых событий в EDWH нужно установить, что доставлены все ожидаемые события, повторные доставки не создали новые факты, а содержимое под прежним ключом не изменилось незаметно. Общая статья о сверках описывает уровни контроля; здесь рассматривается протокол одной партии до агрегации проводок.
Предлагаемый контракт рассчитан на неизменяемые события или явно нумерованные версии. Для выгрузки актуального состояния сначала нужен согласованный снимок; для CDC отдельно определяют порядок изменений и обработку удалений. Сверка партии не доказывает, что источник зарегистрировал все хозяйственные операции. Это материал knowledge, а не описание внедрения или проверенного лабораторного алгоритма.
Зерно ожидаемого набора - одна версия события. Ключ K состоит из системы-источника, учётного контура, идентификатора события и версии, если версии предусмотрены контрактом. Контур включает юридическое лицо и книгу, когда они нужны для уникальности. Номер доставки и batch_id не входят в бизнес-ключ: одно событие может повторно поступить в другой партии. Если событие содержит несколько строк проводок, полноту строк проверяют отдельно по ключу события и номеру строки.
Зерно сырого приёма - одна доставка события с собственным delivery_id. Зерно принятого набора - одна версия события K. Поэтому равенство количества сообщений и количества фактов не является целью при допустимых повторах.
Для каждого денежного поля фиксируют валюту, масштаб и роль суммы. В синтетическом примере ниже сумма строки хранится целыми копейками RUB, дебет положителен, кредит отрицателен. Сравниваются также счёт, сторона и прочие значимые атрибуты: одинаковая сумма ещё не означает одинаковую запись. Валюты не суммируют вместе.
business_date задаёт учётную дату, observed_at - первое получение версии EDWH. Момент начала сверки и отсечка входов сохраняются отдельно. Позднее событие с прежней учётной датой не меняет уже закреплённый ожидаемый набор: оно входит в новую партию или оформленную ревизию партии.
До проверки закрепляют манифест: идентификатор партии и ревизии, область отбора, снимок или диапазон позиций источника, ожидаемые K, версии схемы и преобразования. Если сравнивают payload, закрепляют и ожидаемые значения либо их контрольные представления. Реестр строят на стороне источника для согласованного среза. Реестр, полученный из уже доставленных сообщений, не способен обнаружить потерянное сообщение. Без независимого ожидаемого набора результат формулируют как проверку доставленных данных, а не доказательство полноты.
Сначала определяют значимые поля и их нормализацию, затем сравнивают их. Исходный payload сохраняют для расследования. Транспортные поля вроде времени повторной доставки исключают из логического сравнения, но валюту, масштаб, даты и признак удаления исключать нельзя, если они меняют смысл события. Отсутствующее поле, явный null и пустая строка считаются разными, пока контракт явно не установит обратное.
Для сравнения до преобразования обе стороны приводят к одному контракту источника. Для сравнения после преобразования ожидаемый набор получают по закреплённой версии отображения. Сравнивать сырой JSON с моделью проводок напрямую некорректно: одна запись может породить несколько фактов, а нормализация может менять представление суммы.
RFC 8785 задаёт схему канонизации JSON JCS, включая детерминированную сортировку свойств. Документ имеет статус Informational и не устанавливает бухгалтерские требования. JCS ограничивает представление чисел возможностями IEEE 754 double precision; для точных значений вне этих возможностей нужна согласованная строковая форма или другой контракт. Канонизация не выполняет предметную нормализацию валют, дат и Unicode. Обычная сортировка ключей при сериализации сама по себе не доказывает соответствие JCS.
Хеш канонического содержимого удобен для предварительного сравнения, но совпадение хешей не является математическим доказательством равенства. При требовании точного сравнения сопоставляют сами нормализованные поля или байты. Разные версии канонизации нельзя сравнивать как одну; смена версии требует нового контрольного представления.
Следующий блок - псевдокод, не SQL или Python и не результат выполнения тестов. E - закреплённый ожидаемый набор, R - сырые доставки данной партии. Реестр уже принятых версий A нужен для проверки повторов между партиями.
проверить обязательные ключи и структуру E и R
сгруппировать E и R по K, сохранив количество доставок
для каждого K сравнить все варианты нормализованного содержимого
если у K несколько вариантов, записать конфликт и не выбирать победителя
одинаковые повторы R учесть отдельно, получить один кандидат на K
сравнить кандидатов с ранее принятыми версиями A по K и содержимому
missing = ключи E без доставки в R
extra = ключи R вне E
сравнить содержимое общих неконфликтных K с E
сохранить нарушения и решение о допуске партии
Конфликт в E означает неисправный ожидаемый набор. Доставка с повреждённым ключом регистрируется отдельно и не исчезает при группировке. Конфликт внутри R или с A блокирует принятие соответствующей версии; выбор последней доставки по времени скрыл бы нарушение неизменяемости. При допустимых изменениях требуется новый номер версии и правило последовательности, а не молчаливый выбор payload.
Разности ключей отвечают только на вопрос о наличии. Доставленный конфликтный ключ не считается отсутствующим, но партия не проходит контроль содержимого. После устранения конфликтов должны совпасть уникальные ключи и значимые значения E и принятого набора. Число сырых доставок раскладывают на число представленных ключей и избыточные доставки только для записей с валидными ключами; отклонённые записи учитывают отдельно. Политика допустимых идентичных повторов фиксируется заранее.
Арифметическая иллюстрация, не результат исполнения: E содержит строки A +10000 и B -10000 одного документа в копейках RUB. R содержит две одинаковые доставки A и ни одной B. Сравнение числа сообщений даёт два против двух; проверка ключей обнаруживает missing B и один повтор A. Если вместо второго A пришёл C -10000, общий ноль сохранится, но появятся missing B и extra C.
Сценарий
Ожидаемое решение по предлагаемому контракту
Две доставки K с одинаковым содержимым
Один факт, повтор отражён в протоколе
Две доставки K с разными счетами при одинаковой сумме
Конфликт, автоматического победителя нет
K уже принят в предыдущей партии
Проверить содержимое A, повтор не создаёт новый факт
E и R пусты
Допуск только при подтверждённом пустом срезе источника
Нет манифеста или части его разделов
Полнота не установлена, допуск заблокирован
Удаление передано как tombstone
Сверить ключ, версию и действие, не отбрасывать как пустой payload
Новое событие пришло после отсечки
Новая партия или ревизия, старый протокол сохранён
Баланс документа по валюте проверяют после контроля строк. Пропуск целого сбалансированного документа не нарушит общий баланс, но нарушит полноту ожидаемых ключей.
Для крупных партий применимы группировка и соединения в СУБД или внешняя сортировка по K. Разбиение по стабильной функции от полного K позволяет обрабатывать разделы независимо, если обе стороны используют одинаковое правило. Манифест должен перечислять все разделы, включая пустые; итог собирают только после получения результатов каждого раздела. Выборочная проверка и сравнение суммарных хешей дешевле, но не дают полного списка расхождений и не заменяют точную сверку.
В PostgreSQL 18EXCEPT удаляет дубликаты, а EXCEPT ALL сохраняет различия кратности. Поэтому разность уникальных ключей не проверяет повторы, а сравнение кратности сырых доставок не соответствует модели одного принятого факта на K. Для обнаружения конфликтов сначала группируют варианты содержимого, затем соединяют уникальные кандидаты: соединение двух необработанных наборов с повторами может размножить строки.
В PostgreSQL 18 обычное сравнение с NULL возвращает неизвестное значение; IS DISTINCT FROM позволяет обнаружить отличие, включая случай одного NULL. Это правило сравнения nullable-атрибутов не делает отсутствующий обязательный ключ допустимым. Указанные свойства относятся к PostgreSQL 18; SQL здесь не выполнялся и переносимость на другую СУБД не утверждается.
Протокол сохраняет ревизию манифеста, отсечку, версии правил, число ожидаемых ключей и доставок, повторы, missing, extra, конфликты, повреждённые записи и решение. Для каждого нарушения сохраняют K и ссылки на исходные доставки с контролем доступа. Частичный допуск требует явной политики исключений и списка недопущенных ключей; его нельзя обозначить полной успешной сверкой. Решение о публикации и закрепление принятого набора должны быть согласованы, чтобы параллельная загрузка не изменила входы после проверки.
Before admitting a batch of financial events into an EDWH, establish that all expected events have arrived, repeated deliveries have not created new facts, and content under an existing key has not changed silently. The general reconciliation article describes control layers; this article covers the protocol for one batch before journal entries are aggregated.
The proposed contract applies to immutable events or explicitly numbered versions. A current-state extract first requires an agreed snapshot; CDC also requires rules for change ordering and deletions. Batch reconciliation does not prove that the source recorded every economic transaction. This is a knowledge article, not a description of a deployment or a tested laboratory algorithm.
The expected set has a grain of one event version. Key K consists of the source system, accounting scope, event identifier, and version where the contract supports versions. The scope includes the legal entity and ledger when needed for uniqueness. Delivery identifiers and batch_id are not part of the business key: an event may be replayed in another batch. If an event contains multiple journal lines, line completeness is checked separately using the event key and line number.
Raw ingestion has a grain of one event delivery with its own delivery_id. The accepted set has a grain of one event version K. Equal message and fact counts are therefore not the objective when replays are allowed.
Each monetary field specifies currency, scale, and amount role. In the synthetic example below, a line amount is an integer number of RUB kopecks, with debits positive and credits negative. Accounts, posting sides, and other meaningful attributes are also compared: equal amounts do not establish equal records. Different currencies are not summed together.
business_date is the accounting date; observed_at is when the EDWH first received the version. The reconciliation start time and input cutoff are retained separately. A late event with an earlier accounting date does not change the frozen expected set: it belongs to a new batch or a documented batch revision.
Before checking, freeze a manifest containing the batch and revision identifiers, selection scope, source snapshot or position range, expected keys K, and schema and transformation versions. If payloads are compared, also freeze expected values or their comparison representations. The register is built at the source for the agreed slice. A register derived from messages already delivered cannot detect a lost message. Without an independent expected set, describe the result as a check of delivered data rather than evidence of completeness.
Define meaningful fields and their normalization before comparing them. Retain the original payload for investigation. Transport fields such as replay delivery time are excluded from logical comparison, but currency, scale, dates, and deletion flags must remain when they affect event meaning. An absent field, explicit null, and an empty string are distinct unless the contract explicitly says otherwise.
For comparison before transformation, bring both sides to the same source contract. For comparison after transformation, derive the expected set using the frozen mapping version. Comparing raw JSON directly with a journal model is inappropriate: one record may produce several facts, and normalization may change the representation of an amount.
RFC 8785 defines the JSON Canonicalization Scheme, JCS, including deterministic property sorting. It is an Informational document and imposes no accounting requirements. JCS limits number representation to the capabilities of IEEE 754 double precision; exact values outside those capabilities need an agreed string representation or a different contract. Canonicalization does not perform domain normalization of currencies, dates, or Unicode. Merely sorting keys during serialization does not establish JCS compliance.
A hash of canonical content is useful for preliminary comparison, but matching hashes are not mathematical proof of equality. Where exact comparison is required, compare normalized fields or bytes themselves. Different canonicalization versions cannot be treated as one; changing the version requires a new comparison representation.
The following block is pseudocode, not SQL or Python and not a test execution result. E is the frozen expected set, and R contains raw deliveries for this batch. Register A of previously accepted versions is needed to check replays across batches.
validate required keys and the structure of E and R
group E and R by K, retaining delivery counts
for each K, compare all normalized content variants
if K has multiple variants, record a conflict without selecting a winner
record identical R replays separately and produce one candidate per K
compare candidates with previously accepted versions A by K and content
missing = E keys with no delivery in R
extra = R keys outside E
compare content for common nonconflicting keys K against E
retain violations and the batch admission decision
A conflict in E means the expected set is defective. A delivery with a malformed key is recorded separately and must not disappear during grouping. A conflict within R or against A blocks acceptance of that version; choosing the latest delivery would hide an immutability violation. Permitted changes require a new version number and ordering rules rather than a silent payload choice.
Key differences answer only the question of presence. A delivered conflicting key is not classified as missing, but the batch fails the content check. Once conflicts are resolved, the unique keys and meaningful values in E and the accepted set must match. The raw delivery count can be decomposed into represented keys and excess deliveries only for records with valid keys; rejected records are counted separately. The policy for permitted identical replays is fixed in advance.
This is an arithmetic illustration, not an execution result: E contains lines A +10000 and B -10000 of one document, measured in RUB kopecks. R contains two identical deliveries of A and none of B. Message counts are two against two; key checks detect missing B and one replay of A. If C -10000 arrives instead of the second A, the overall total remains zero, but missing B and extra C are detected.
Scenario
Expected decision under the proposed contract
Two deliveries of K with identical content
One fact, with the replay recorded
Two deliveries of K with different accounts but equal amounts
Conflict, with no automatic winner
K was accepted in a previous batch
Compare with A; the replay creates no new fact
E and R are empty
Admit only when the source confirms an empty slice
The manifest or some of its partitions are missing
Completeness is unestablished; admission is blocked
A deletion arrives as a tombstone
Compare key, version, and action; do not discard it as an empty payload
A new event arrives after the cutoff
Use a new batch or revision and retain the previous record
Document balance by currency is checked after line controls. Losing an entire balanced document does not disturb the overall balance, but violates completeness of expected keys.
Large batches can use grouping and joins in a database or external sorting by K. Partitioning with a stable function of the complete K allows independent processing if both sides use the same rule. The manifest must list all partitions, including empty ones; the final result is assembled only after every partition has returned its result. Sampling and aggregate hash comparisons are cheaper, but do not provide a complete discrepancy list and do not replace exact reconciliation.
In PostgreSQL 18, EXCEPT removes duplicates, whereas EXCEPT ALL preserves multiplicity differences. A difference of unique keys therefore does not check replays, while comparing raw delivery multiplicities does not match the model of one accepted fact per K. To detect conflicts, first group content variants and then join unique candidates: joining two unprocessed sets containing duplicates can multiply rows.
In PostgreSQL 18, ordinary comparisons with NULL return an unknown value; IS DISTINCT FROM detects a difference, including when only one side is NULL. This rule for comparing nullable attributes does not make a missing mandatory key acceptable. These properties concern PostgreSQL 18; no SQL was executed here, and portability to another database is not claimed.
The record retains the manifest revision, cutoff, rule versions, expected key and delivery counts, replays, missing and extra keys, conflicts, malformed records, and the decision. For each violation, retain K and access-controlled references to original deliveries. Partial admission requires an explicit exception policy and a list of excluded keys; it must not be labeled a fully successful reconciliation. The publication decision and freezing of the accepted set must be coordinated so that a concurrent load cannot change the inputs after checking.