docs(orders): документация перестроена вокруг текущей модели

- Зачем:
  - описание вело нового читателя через историю исправления вместо действующего
    контракта слоёв.
- Что:
  - прямо описаны версия заказа, границы пакетного приёма и расчёт окна DDS.
  - ответственность ODS, DDS и конвейера разделена без ссылок на прежние
    ошибки как на основной довод.
  - исторический вывод исследования помечен до устаревшего абзаца, а ссылки
    ведут к отдельным решениям и контрактам.
- Проверка:
  - make lint.
  - make config-test.
This commit is contained in:
ddadmin committed 2026-08-26 18:47:22 +03:00
1 parent a82c75705d
commit a9cead2974
6 files changed
+68 -74

No files matched your search

+8 -8
View File
@@ -65,15 +65,15 @@ ODS: это единственный устойчивый ключ сущнос
одинаковы у всех строк дня и отвечают на вопрос «каким запуском и когда
собран этот день».
Ярлыки ODS — `snapshot_date`, `_load_id` приёма, `_load_ts` прибытия — в
модель не переезжают. Лгать они перестали: колонкой версии в ODS стала дата
слепка, и у выжившей строки она равна последнему слепку, в котором заказ был
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Довод стал проще:
читателя у этих колонок здесь нет — модель отвечает на вопрос «что сейчас
известно о заказе», а не «когда его показывал источник». Отсюда правило слоя:
Ярлыки ODS (`snapshot_date`, `_load_id` приёма, `_load_ts` прибытия) в модель
не переезжают. Они описывают наблюдение и доставку строки в ODS. Модель DDS
хранит текущее деловое состояние заказа. В ODS `snapshot_date` задаёт версию и
называет последний слепок, в котором источник показал заказ
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Дату
наблюдения и координаты доставки читатель спрашивает у ODS по
`order_id`. Правило слоя:
**служебные колонки — координаты собственной загрузки слоя; чужие координаты
спрашиваются у их слоя по ключу сущности**. Этим закрыто наследство #85:
читатель у чужих ярлыков выше ODS не нашёлся и при названных читателях модели.
спрашиваются у их слоя по ключу сущности**.
Происшествие качества данных расследуется цепочкой, где каждый шаг
спрашивает свой слой на его языке: по `_load_id` строки DDS видно, какой
+19 -17
View File
@@ -5,10 +5,9 @@
движки, что именно пересобирается — живут в доках слоя
([карта модели DDS](dds/README.md)); здесь — только конвейер.
Спека обещала `etl_pipeline` этапом 5 — он понадобился раньше. Конвейер
рождается этапом 4, и временного в нём ничего нет: дневная сборка вперёд
останется и после. Этапу 5 достаётся его законный предмет — партиционная
переобработка, умение переиграть произвольный прошлый день по команде.
Дневная сборка вперёд появляется на этапе 4 и остаётся основным режимом
конвейера. На этапе 5 к ней добавляется партиционная переобработка: запуск
произвольного прошлого дня по команде.
## Форма
@@ -47,25 +46,28 @@
Поэтому один и тот же запуск одинаково верен из триггера, с кнопки менти и
после падения, а разгону на семь дней хватает одного прогона.
Окно заказов — **дни, которые нёс последний слепок**. Называет их сам
источник: `snapshot_date` выжившей строки равен последнему слепку, в котором
заказ был, поэтому оба края окна трансформация читает прямо из ODS
([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Своего числа окна у
стенда нет, и расходиться с миром нечему.
Окно заказов задаёт последний слепок, наблюдаемый в ODS. У каждого заказа
`snapshot_date` равен последнему слепку, в котором источник его показал.
Трансформация выбирает строки с максимальной `snapshot_date` и берёт среди них
минимальный и максимальный `order_date`. Это границы дней, состояние которых
источник ещё может изменить
([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Кроме живого окна,
трансформация закрывает пропуски: находит самый ранний день, представленный в
ODS и ещё не собранный в DDS, затем при необходимости расширяет до него левую
границу.
Позиция мира заказам не отвечает: она говорит, какой день прожит, а не какой
доехал. Прогон дня D везёт слепок дня D − 1, да и ставится позиция раньше
трансформации, так что после неудачного приёма обгоняет хранилище. Слой обязан
считать объём по тому, что у него есть, а не по тому, что мир о себе заявляет.
У сессий всё наоборот: им отвечает только позиция, потому что вопрос у них
другой. Им нужен не день, о котором рассказал источник, а день, который мир
**прожил**: события живой день от прожитого не отличают — и посреди дня D,
и сразу после его конца позднейший день событий равен D. Соберись сессии посреди
дня, визит, начатый в 23:50, остался бы обрезанным навсегда: замёрзший день слой
не пересобирает.
Отсюда правило — последний прожитый день равен первому дню событий плюс позиция
минус один ([`sql/dds/session_scope.sql`](../../sql/dds/session_scope.sql)).
Для сессий границу задаёт `world_position`: она отличает полностью прожитый
день от ещё идущего. Сами события этой границы не показывают: и посреди дня D,
и сразу после его конца их позднейший день равен D. Если собрать сессии посреди
дня, визит, начатый в 23:50, останется обрезанным: завершённый день слой
повторно не собирает. Поэтому последний прожитый день равен первому дню
событий плюс позиция минус один
([`sql/dds/session_scope.sql`](../../sql/dds/session_scope.sql)).
Оговорка про «доехал» остаётся в силе и здесь: за хвост потока отвечает не
позиция, а «Готовность источника».
+22 -26
View File
@@ -7,10 +7,10 @@
## Проблема
Заказы приезжают полным слепком окна изменяемости, но Kafka передаёт его
отдельными сообщениями и не сообщает потребителю, где слепок закончился. Прямое
чтение Kafka Engine возвращает одну порцию. Поэтому прежняя публикация через
`REPLACE PARTITION snapshot_date` приравнивала дату наблюдения к отсутствующей
транспортной границе и могла заменить день неполным набором строк.
отдельными сообщениями и не сообщает потребителю, где слепок закончился.
Прямое чтение Kafka Engine возвращает одну порцию без признака полноты.
Поэтому `snapshot_date` не может служить границей публикации: `REPLACE
PARTITION` могла бы заменить партицию дня неполным набором строк.
Одновременно типизированный ODS не должен принимать правдоподобные значения по
умолчанию из грязного JSON или останавливать весь пакет из-за одной строки.
@@ -81,11 +81,11 @@ JSON-объектом с точным набором ключей: `order_id`, `
малый контракт заказа нет причины. Граница строгости заканчивается на форме
провода: содержимое позиций и бизнес-инварианты намеренно остаются ниже.
Одно условие контракта предикатом не проверить, и названо оно всё равно
здесь: **одна дата — один слепок**. Вторую выгрузку того же дня приём не
отличит от первой, а колонка версии — дата слепка — на ней даёт ничью.
Модельный источник выгружает день один раз; настоящему такое условие ставят
договором, а не предикатом
Контракт источника задаёт частоту: **одной дате соответствует один слепок**. Это
свойство всего слепка, поэтому предикат одной строки его не проверяет. Вторая
выгрузка того же дня даст ничью по `snapshot_date`, и выбор строки потеряет
однозначность. Модельный источник выполняет условие; в настоящей интеграции
его фиксируют в договоре с источником
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)).
Класс брака выбирается первым совпадением:
@@ -113,11 +113,11 @@ JSON-объектом с точным набором ключей: `order_id`, `
- `_load_id` — какой запуск Airflow принял строку;
- `_load_ts` — когда строка приехала в хранилище.
Версию выбирает `snapshot_date`: выживает строка позднейшего слепка
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Поэтому ярлык
выжившей строки — правдивый ответ на вопрос «когда заказ видели в последний
раз», и по нему трансформация DDS читает объём пересборки. `_load_id` и
`_load_ts` выжившей строки говорят про ту же доставку.
Версию выбирает `snapshot_date`: выживает строка с максимальной датой слепка
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Поэтому
`snapshot_date` показывает последнее наблюдение заказа. По строкам последней
даты слепка DDS определяет окно пересборки. `_load_id` и `_load_ts` принадлежат
той физической строке, которую сохранило слияние, и описывают её доставку.
Схема версий стоит на дисциплине источника: слепок с поздней датой не
показывает состояния старше. Модельный мир её держит — состояние читается на
@@ -211,19 +211,15 @@ dbt snapshots — стратегия `check` вместо `timestamp`.
`system.query_log` на первой ноде осталось ровно по одному завершённому запросу
пользователя `etl` для забора в STG, записи годных строк и записи брака.
Дисциплина источника замерена 22 августа 2026 года при исполнении #137: во
всех 3034 парах строк с равным `updated_at` деловые поля совпали побайтово, по
слепкам метка ни разу не убывала, а `ods.order_v` сошёлся построчно с эталоном
`argMax` по `updated_at`, посчитанным прямо из сырья. Тем же замером виден
произвол ярлыка при версии по деловой метке — довод, на котором стоит
[ADR 0014](../../adr/0014-order-version-snapshot-date.md).
Дисциплина источника замерена 22 августа 2026 года при исполнении #137.
Во всех 3034 парах строк с равным `updated_at` деловые поля совпали
побайтово, а по слепкам метка ни разу не убывала.
Правдивость ярлыка снята на пересобранном мире 26 августа 2026 года при
исполнении #165. После двух слепков `snapshot_date` каждой из 1917 строк
`ods.order_v` равен последнему слепку, который нёс её заказ: расхождений ноль
при 3356 физически лежащих строках — выбор был не единственным. Деловое
содержимое сменой колонки версии не тронулось: те же строки построчно совпали
с эталоном `argMax` по `updated_at` из сырья по всем полям.
Текущее состояние проверено на пересобранном мире 26 августа 2026 года при
исполнении #165. После двух слепков физическая таблица содержала 3356 версий
1917 заказов. У всех строк `ods.order_v` дата слепка совпала с последним
слепком заказа; деловые поля совпали с эталоном `argMax` по `updated_at` из
сырья. В обеих сверках расхождений нет.
Забор из Kafka в STG снят на живом стенде 18 августа 2026 года при исполнении
#93: одно прямое чтение приносит весь слепок дня, метаданные доставки доступны,
+7 -6
View File
@@ -234,12 +234,13 @@ ODS и в таблицу ошибок. Нужен ли он выше ODS, реш
версии, которые ещё не убрали фоновые слияния, и не является архивом истории.
`ods.order_v` служит поверхностью точного текущего состояния для следующих
слоёв. Представление сохраняет язык источника и не решает, какой станет модель
DDS. `snapshot_date` в нём — дата наблюдения и колонка версии, но не ключ
публикации. Полное решение — в
[ADR 0010](../adr/0010-order-versions-in-ods.md), колонка версии —
в [ADR 0014](../adr/0014-order-version-snapshot-date.md); механика приёма —
в [спецификации приёма заказов](orders/ingestion.md). Какой моделью заказ
живёт дальше — [модель заказа в DDS](dds/order.md).
DDS. `snapshot_date` в нём одновременно дата наблюдения и колонка версии. Она
не доказывает полноту принятой порции, поэтому ODS не публикует партиции по
дате слепка. Модель версионного ODS записана в
[ADR 0010](../adr/0010-order-versions-in-ods.md), выбор колонки версии описан в
[ADR 0014](../adr/0014-order-version-snapshot-date.md), пакетный приём описан в
[спецификации приёма заказов](orders/ingestion.md). Контракт следующего слоя
задан в [модели заказа в DDS](dds/order.md).
## Часовые пояса
@@ -126,6 +126,10 @@ parseDateTime64InJodaSyntaxOrNull(
использовать и для типизированной строки, и для маршрутизации ошибки; нулевая
дата не нужна.
> Актуальный стенд: версию `ods.order` задаёт `snapshot_date`
> ([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Следующий
> абзац фиксирует исходный вывод исследования и не описывает текущий контракт.
`updated_at` допустим как колонка версии `ReplacingMergeTree`: ClickHouse
явно разрешает для `ver` тип `DateTime64` и оставляет строку с максимальной
версией
@@ -135,12 +139,6 @@ parseDateTime64InJodaSyntaxOrNull(
монотонные миллисекундные `updated_at` на один заказ; отдельный счётчик версий
без такого сценария был бы лишним.
> Вывод о колонке версии пересмотрен: версию `ods.order` задаёт `snapshot_date`
> ([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Там же разобрано,
> почему обещанию документации про ничью — «побеждает вставленная позже» —
> верить нельзя: замер на стенде его опроверг. Абзац выше оставлен как был,
> документ историчен.
## Дата слепка и константы мира
Периодический слепок имеет зерно заранее заданного периода — например, дня, —
+8 -11
View File
@@ -222,10 +222,11 @@ Ecommerce (заполнены только у торговых событий):
- `ods.order` принимает версии заказа в
`ReplacingMergeTree(snapshot_date)`: `ORDER BY order_id`, партиция по дню
неизменного `created_at`, шардирование по `cityHash64(order_id)`. Дата
слепка служит колонкой версии — выживает строка позднейшего слепка
([ADR 0014](../adr/0014-order-version-snapshot-date.md)), — но публикацию
и идемпотентность по-прежнему не задаёт. Физическое чтение может видеть
несколько версий; `ods.order_v` возвращает точное текущее состояние. В DDS одна строка хранит
слепка служит колонкой версии: выживает строка с максимальной
`snapshot_date` ([ADR 0014](../adr/0014-order-version-snapshot-date.md)).
Дата не доказывает полноту принятой порции, поэтому ODS не заменяет партицию
слепка целиком. Физическое чтение может видеть несколько версий;
`ods.order_v` возвращает точное текущее состояние. В DDS одна строка хранит
один заказ с позициями в типизированных массивах; физическая модель и
пакетное наполнение описаны в [доке заказа](../architecture/dds/order.md).
@@ -453,13 +454,9 @@ README.
матвью к нему не привязана, и сырьё забирает шаг, которым управляет Airflow —
одно прямое чтение вставляет порцию в `stg.orders_raw` с `_load_id` запуска.
Один следующий `task_id` двумя последовательными запросами пишет годные строки
и ошибки из того же среза. `ods.order` хранит версии по дате слепка
([ADR 0014](../adr/0014-order-version-snapshot-date.md)), а не публикует
партицию `snapshot_date`. Слой сырья остаётся точкой повтора и
разбора одной принятой порции.
Стенд получает от этого два режима приёма рядом, поток и слепок, и сравнение
из опорных точек раздела 12 переформулировано под них.
и ошибки из того же среза. `ods.order` выбирает текущую версию по максимальной
`snapshot_date` ([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Слой
сырья остаётся точкой повтора и разбора одной принятой порции.
Состав служебных колонок задаёт дока хранилища. Спеке важны два следствия:
`ods.event` и `ods.order` получают метку загрузки `_load_ts`, но версию