- Зачем:
- ярлык 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>
135 lines
11 KiB
Markdown
135 lines
11 KiB
Markdown
# Модель заказа в DDS
|
||
|
||
Учебный результат: менти проектирует бизнес-сущность поверх готового ODS —
|
||
выбирает зерно, состав строки и способ наполнения — и видит идемпотентность
|
||
трансформации, устроенную иначе, чем в приёме: заменой партиции, а не
|
||
версиями.
|
||
|
||
## Зерно
|
||
|
||
Одна строка — заказ в текущем состоянии, ключ — `order_id`. Модель отвечает
|
||
на вопрос «что сейчас известно о заказе»; историю версий не хранит:
|
||
читателя у истории нет ни в одной витрине, а ODS её и не обещает —
|
||
промежуточные версии съедают фоновые слияния. «Дыхание» выручки внутри окна
|
||
K живёт на текущем состоянии само: пересборка дня меняет статусы заказов, и
|
||
число дня меняется вместе с ними.
|
||
|
||
## Состав строки
|
||
|
||
Деловые поля источника переезжают из `ods.order_v` без переименований —
|
||
бэкенд уже говорит в snake_case: `order_id`, `user_id`, `status`, деньги в
|
||
`Decimal` (`items_total`, `discount`, `delivery`, `total`).
|
||
|
||
Времена `created_at` и `updated_at` пересчитываются при загрузке в пояс
|
||
счётчика: DDS обслуживает человека с дашбордом ([дока
|
||
хранилища](../storage.md), «Часовые пояса»). Там же один раз считается
|
||
**день заказа** — колонка `order_date`, по образцу `EventDate` у события:
|
||
суточные срезы группируются по готовой дате и пояса не упоминают.
|
||
|
||
Позиции разобраны из сырой строки `items` в три типизированных массива
|
||
одной длины — артикул, количество, цена (`Decimal`). Это родная для
|
||
ClickHouse денормализация: позиция не живёт без заказа и собственного ключа
|
||
не имеет, а разворачивает массивы читатель — `ARRAY JOIN` в витрине
|
||
выручки, с обогащением категорией через `dictGet`
|
||
([мастер-спека](../../specs/2026-07-30-stand-v2-realism.md), раздел 7). Разбор
|
||
JSON происходит один раз, на границе ODS → DDS, куда его и положила спека
|
||
(раздел 2); записывать его параллельными колонками-массивами или `Nested` —
|
||
выбор DDL, не модели.
|
||
|
||
## Движок и наполнение
|
||
|
||
`dds.order_rep` — `ReplicatedMergeTree` без колонки версии: партиция —
|
||
`order_date` (день заказа неизменен), ключ сортировки — `order_id`.
|
||
`dds.order_dist` шардирует по `cityHash64(order_id)`, тем же выражением, что
|
||
ODS: это единственный устойчивый ключ сущности.
|
||
|
||
Идемпотентность держит замена партиции, а не версии. Сбор дня — батч после
|
||
прихода слепка: трансформация собирает партицию заново из `ods.order_v` и
|
||
атомарно подменяет. Поэтому каждая строка физической таблицы — ровно один
|
||
заказ, а чтению не нужен `FINAL`. Пока окно K живо, день «дышит» — статусы
|
||
заказов меняются, и партиция дня пересобирается очередным запуском; на выходе
|
||
из окна день замерзает.
|
||
|
||
Публичная поверхность — `dds.order_v`: обычное представление с явным списком
|
||
деловых и собственных служебных колонок над `dds.order_dist`, без `FINAL`.
|
||
Роли объектов и доступ следуют общей [конвенции](../storage.md), «Имена
|
||
объектов».
|
||
|
||
Механика запуска — где собирается партиция и как подменяется на кластере —
|
||
сюда не входит: это устройство [дневного конвейера](../etl.md).
|
||
|
||
## Служебные колонки
|
||
|
||
Строка получает `_load_id` и `_load_ts` запуска трансформации, собравшего
|
||
её партицию, — по общей конвенции хранилища. При замене партиции они
|
||
одинаковы у всех строк дня и отвечают на вопрос «каким запуском и когда
|
||
собран этот день».
|
||
|
||
Ярлыки ODS — `snapshot_date`, `_load_id` приёма, `_load_ts` прибытия — в
|
||
модель не переезжают. Лгать они перестали: колонкой версии в ODS стала дата
|
||
слепка, и у выжившей строки она равна последнему слепку, в котором заказ был
|
||
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Довод стал проще:
|
||
читателя у этих колонок здесь нет — модель отвечает на вопрос «что сейчас
|
||
известно о заказе», а не «когда его показывал источник». Отсюда правило слоя:
|
||
**служебные колонки — координаты собственной загрузки слоя; чужие координаты
|
||
спрашиваются у их слоя по ключу сущности**. Этим закрыто наследство #85:
|
||
читатель у чужих ярлыков выше ODS не нашёлся и при названных читателях модели.
|
||
|
||
Происшествие качества данных расследуется цепочкой, где каждый шаг
|
||
спрашивает свой слой на его языке: по `_load_id` строки DDS видно, какой
|
||
запуск собрал день; по `order_id` ODS показывает физические версии и брак —
|
||
там свой `_load_id` приёма связывает годные строки, ошибки и сырьё; дальше
|
||
сырьё STG, пока живо. Окно расследования задано сроками хранения: сырьё —
|
||
трое суток, брак — месяц ([дока хранилища](../storage.md)).
|
||
|
||
## Читатели
|
||
|
||
- Витрина выручки `dm.revenue_daily_v`: заказы в `paid`, `ARRAY JOIN`
|
||
позиций, категория через `dictGet` (спека, раздел 7).
|
||
- Сверка `dm.purchase_vs_orders_v`: `order_id`, `order_date`, `items_total`,
|
||
`status`; соединение — GLOBAL JOIN с обеими сторонами-подзапросами
|
||
([исследование соединений](../../research/2026-08-22-distributed-join.md)).
|
||
- Сборка карты идентичностей: пара `order_id` ↔ `user_id`, источник —
|
||
`dds.order_v`.
|
||
|
||
## Не входит
|
||
|
||
- Механика запуска трансформации и замены партиций на кластере —
|
||
[дневной конвейер](../etl.md).
|
||
- Контур проверок качества (даг DQ, `dm.dq_summary`) — отдельный разговор
|
||
этапа 4.
|
||
- Инкрементальное наполнение вместо пересборки — техдолг #2.
|
||
|
||
## Отклонённые варианты
|
||
|
||
- **Одно динамическое представление вместо физической таблицы.** Рамка стенда —
|
||
показать модель данных, которую хранилище готовит потребителям; урок
|
||
«слой — контракт, а не копия» уже закреплён за `dds.event_v`, а разбор
|
||
позиций и пересчёт времён при каждом чтении собирали бы модель заново на
|
||
лету. Публичное `dds.order_v` поверх готовой физической модели этому доводу
|
||
не противоречит: оно ничего заново не вычисляет.
|
||
- **`ReplacingMergeTree` ещё раз.** Повторил бы механику ODS вторым разом:
|
||
физически хранил бы устаревшие состояния, вернул бы `FINAL`
|
||
и обёртку-представление — а урок версий менти уже отыграл.
|
||
- **Отдельная сущность `dds.order_item`.** Вторая таблица, второй вопрос
|
||
шардирования и `JOIN` там, где хватает `ARRAY JOIN`; нормализация из
|
||
реляционного мира, а не модель под колоночное хранилище. `JOIN` менти
|
||
увидит в сверке, работу с массивами — только здесь.
|
||
- **Зерно «версия заказа» (история).** Читателя нет; ODS историю не
|
||
обещает, а мир при нужде переигрывается генератором.
|
||
- **Перенос ярлыков ODS** — см. «Служебные колонки».
|
||
- **Без модели вовсе: витрины читают `ods.order_v` напрямую.** Разбор JSON
|
||
и пересчёт времён расползлись бы по витринам, а модель данных — то, ради
|
||
чего сущность и строится, — не была бы показана.
|
||
|
||
## Связанные решения
|
||
|
||
- [ADR 0010](../../adr/0010-order-versions-in-ods.md) — версии заказов в
|
||
ODS, устойчивая поверхность `ods.order_v`.
|
||
- [Приём заказов](../orders/ingestion.md) — граница строгого приёма и
|
||
четыре координаты строки ODS.
|
||
- [Проталкивание условий сквозь
|
||
представления](../../research/2026-08-22-view-predicate-pushdown.md) —
|
||
резолюция #128 той же карты: цена `FINAL` и цепочек представлений, фон, на
|
||
котором здесь выбрана физическая таблица.
|