feat(ods): версией заказа стала дата слепка, окно DDS — из данных

- Зачем:
  - ярлык snapshot_date выжившей строки доставался жребием, а объём
    пересборки DDS держался на константе окна с двумя несверяемыми копиями
    (ADR 0014, развилка #163).
- Что:
  - ods.order_rep переведена на ReplacingMergeTree(snapshot_date): выживает
    строка позднейшего слепка, ярлык стал правдивым.
  - order_scope.sql выводит окно из дней последнего слепка; ORDER_WINDOW_DAYS
    и параметр window_days сняты со стенда, мёртвая защита краем источника
    убрана.
  - документы и комментарии SQL приведены к ADR 0014: роль ODS, условие
    «одна дата — один слепок», объём прогона и слепые пятна конвейера.
- Проверка:
  - make lint; make rebuild-storage, затем make smoke, make check-clickhouse,
    make check-services — зелёные.
  - после двух слепков snapshot_date всех 1917 строк ods.order_v равен
    последнему слепку заказа, расхождений ноль; деловые поля построчно
    совпали с эталоном argMax по updated_at из сырья.
  - dds_transform без K: первый прогон 06-01…06-07, после нового дня мира
    06-02…06-08, повтор прогона содержимого не изменил.

Closes #165

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ddadminandClaude Opus 5 committed 2026-08-26 17:28:14 +03:00
1 parent 650efacc3f
commit a82c75705d
13 files changed
+140 -141

No files matched your search

+1 -1
View File
@@ -38,7 +38,7 @@
[мост к склейке](identity.md).
- **Приём — пакетный забор**: одно прямое чтение Kafka в STG, два
`INSERT SELECT` в типизированный ODS и таблицу ошибок; `ods.order`
принимает версии заказа на `ReplacingMergeTree(updated_at)` —
принимает версии заказа на `ReplacingMergeTree(snapshot_date)` —
[приём из Kafka в ODS](ingestion.md).
- **Дальше заказ живёт моделью DDS** — уже по хранилищную сторону границы,
в наборе слоя: [модель заказа](../dds/order.md).
+40 -23
View File
@@ -19,8 +19,8 @@
- сохранить пакетный забор как контраст потоковому приёму событий;
- один раз принять байты в STG и независимо разложить строки на годные и брак;
- хранить в ODS типизированные версии заказов, не привязывая идемпотентность к
`snapshot_date`;
- хранить в ODS типизированные версии заказов, не публикуя слепок заменой
партиции;
- дать следующим слоям один корректный способ прочитать текущее состояние;
- оставить код проверки коротким и ограничить его контрактом провода.
@@ -81,6 +81,13 @@ JSON-объектом с точным набором ключей: `order_id`, `
малый контракт заказа нет причины. Граница строгости заканчивается на форме
провода: содержимое позиций и бизнес-инварианты намеренно остаются ниже.
Одно условие контракта предикатом не проверить, и названо оно всё равно
здесь: **одна дата — один слепок**. Вторую выгрузку того же дня приём не
отличит от первой, а колонка версии — дата слепка — на ней даёт ничью.
Модельный источник выгружает день один раз; настоящему такое условие ставят
договором, а не предикатом
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)).
Класс брака выбирается первым совпадением:
1. `not_an_object`;
@@ -94,31 +101,34 @@ JSON-объектом с точным набором ключей: `order_id`, `
## Роль ODS
`ods.order_rep` хранит физически принятые версии в
`ReplacingMergeTree(updated_at)`. Ключ сортировки — `order_id`, партиция — день
неизменного `created_at`. `ods.order_dist` шардирует по
`ReplacingMergeTree(snapshot_date)`. Ключ сортировки — `order_id`, партиция —
день неизменного `created_at`. `ods.order_dist` шардирует по
`cityHash64(order_id)`: только так все версии заказа попадают на один шард и
`FINAL` даёт корректный результат через распределённую таблицу.
Четыре координаты отвечают на разные вопросы:
- `updated_at` — какая бизнес-версия заказа новее;
- `snapshot_date` — в слепке какого модельного дня источник показал строку;
- `updated_at` — когда строку последний раз меняли в источнике;
- `_load_id` — какой запуск Airflow принял строку;
- `_load_ts` — когда строка приехала в хранилище.
Версию выбирает только `updated_at`. Заказ, не менявшийся несколько дней,
приезжает в нескольких слепках одинаковыми деловыми полями, и какая из этих
строк переживёт схлопывание — не обещано. Поэтому `snapshot_date` выжившей
строки — один из дней, когда источник показывал эту версию, не обязательно
последний; то же с `_load_id` и `_load_ts`. Вопрос «когда заказ наблюдался в
последний раз» задаётся сырью STG, пока то живо, — а не одной строке ODS.
Версию выбирает `snapshot_date`: выживает строка позднейшего слепка
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Поэтому ярлык
выжившей строки — правдивый ответ на вопрос «когда заказ видели в последний
раз», и по нему трансформация DDS читает объём пересборки. `_load_id` и
`_load_ts` выжившей строки говорят про ту же доставку.
Сама схема версий стоит на допущении об источнике: уточнение заказа поднимает
`updated_at`. Генератор моделирует источник с триггерной меткой и допущение
выполняет (замер — «Что проверено»); источник, умеющий уточнить строку тихо —
ручным `UPDATE` мимо приложения, миграцией, — ломает его, и тогда версии по
метке времени мало: нужна сверка самих полей, в терминах dbt snapshots —
стратегия `check` вместо `timestamp`.
Схема версий стоит на дисциплине источника: слепок с поздней датой не
показывает состояния старше. Модельный мир её держит — состояние читается на
границе суток, и слепок дня выходит чистой функцией зерна и дня; источник,
собирающий слепок из отставшей реплики, её ломает. Деловая метка `updated_at`
при этом монотонна по слепкам, поэтому выбор по дате слепка даёт тот же ответ,
что выбор по метке (замер — «Что проверено»). Тихая правка строки — ручным
`UPDATE` мимо приложения, миграцией — метку не двигает, но следующий слепок
перекроет её сам, пока заказ в окне изменяемости. За окном источник заказ
больше не показывает, и там помогает только сверка самих полей, в терминах
dbt snapshots — стратегия `check` вместо `timestamp`.
Обычное чтение `_dist` показывает физически сохранившиеся версии и нужно для
диагностики. Их число зависит от фоновых слияний: ODS не служит архивом истории.
@@ -201,12 +211,19 @@ JSON-объектом с точным набором ключей: `order_id`, `
`system.query_log` на первой ноде осталось ровно по одному завершённому запросу
пользователя `etl` для забора в STG, записи годных строк и записи брака.
Допущение и произвол ярлыков замерены 22 августа 2026 года при исполнении
#137: во всех 3034 парах строк с равным `updated_at` деловые поля совпали
побайтово, по слепкам метка ни разу не убывала, а `ods.order_v` сошёлся
построчно с эталоном `argMax` по `updated_at`, посчитанным прямо из сырья.
Произвол ярлыков виден въявь: у 86 заказов стартового мира выжила строка
серединного слепка — не первого и не последнего.
Дисциплина источника замерена 22 августа 2026 года при исполнении #137: во
всех 3034 парах строк с равным `updated_at` деловые поля совпали побайтово, по
слепкам метка ни разу не убывала, а `ods.order_v` сошёлся построчно с эталоном
`argMax` по `updated_at`, посчитанным прямо из сырья. Тем же замером виден
произвол ярлыка при версии по деловой метке — довод, на котором стоит
[ADR 0014](../../adr/0014-order-version-snapshot-date.md).
Правдивость ярлыка снята на пересобранном мире 26 августа 2026 года при
исполнении #165. После двух слепков `snapshot_date` каждой из 1917 строк
`ods.order_v` равен последнему слепку, который нёс её заказ: расхождений ноль
при 3356 физически лежащих строках — выбор был не единственным. Деловое
содержимое сменой колонки версии не тронулось: те же строки построчно совпали
с эталоном `argMax` по `updated_at` из сырья по всем полям.
Забор из Kafka в STG снят на живом стенде 18 августа 2026 года при исполнении
#93: одно прямое чтение приносит весь слепок дня, метаданные доставки доступны,