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