docs(dds): спроектирована модель заказа в DDS и заведён набор доков слоя
- Зачем: - развилка #130 карты #127: заказу нужен объект в DDS — зерно, состав, движок и судьба служебных колонок; резолюции этапа 4 нужен дом в git. - Что: - создан набор docs/architecture/dds/ по ADR 0011: README с картой модели и правилами слоя, order.md с моделью заказа — зерно «заказ в текущем состоянии», позиции массивами, MergeTree с заменой партиций, свои _load_id/_load_ts вместо ярлыков ODS (закрывает наследство #85); - указатели на модель добавлены в orders/README.md, orders/ingestion.md и storage.md. - Проверка: - только документы; относительные ссылки сверены с деревом docs/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1adacc6d7c
commit
7972847601
5 files changed
+192
-3
No files matched your search
@@ -0,0 +1,58 @@
|
|||||||
|
# Слой DDS: бизнес-модель хранилища
|
||||||
|
|
||||||
|
Устройство детального слоя: сущности, которыми хранилище говорит с
|
||||||
|
витринами, их связи и правила слоя. Набор собирается развилками карты #127
|
||||||
|
(этап 4) и правится по мере постройки.
|
||||||
|
|
||||||
|
Рамки заданы мастер-спекой [«Боевой реализм стенда
|
||||||
|
(v2)»](../../specs/2026-07-30-stand-v2-realism.md): карта таблиц и витрины —
|
||||||
|
раздел 7, сверка — раздел 4, склейка идентичностей — раздел 5. Дом выбран
|
||||||
|
на первой развилке карты: резолюции этапа 4 живут здесь, по хранилищную
|
||||||
|
сторону границы «источник | хранилище». Устройство источников остаётся их
|
||||||
|
собственным наборам ([заказы бэкенда](../orders/README.md)) — генератор
|
||||||
|
переносим и о нашем DDS не знает.
|
||||||
|
|
||||||
|
## Карта модели
|
||||||
|
|
||||||
|
Целостность модели — связи и ко-локация — живёт в этом разделе; файлы
|
||||||
|
сущностей держат глубину и на карту ссылаются.
|
||||||
|
|
||||||
|
- **Заказ** (`dds.order`) — физическая сущность: одна строка — заказ в
|
||||||
|
текущем состоянии, позиции массивами в строке, наполняется заменой
|
||||||
|
партиций — [модель заказа](order.md).
|
||||||
|
- **Сессия** (`dds.session`) — зерно и правила сборки решает развилка #131.
|
||||||
|
- **Событие** (`dds.event_v`) — контракт над ODS, а не копия: язык
|
||||||
|
бизнес-модели событию даёт развилка #132.
|
||||||
|
- **Карта идентичностей** (`dds.identity_map`) — кука ↔ пользователь
|
||||||
|
магазина; сборку решает развилка #133.
|
||||||
|
|
||||||
|
Связи держатся двумя ключами моста (спека, разделы 4 и 5): `purchaseID[1]`
|
||||||
|
события равен `order_id` заказа — по нему идут сверка и сборка склейки;
|
||||||
|
`user_id` заказа связывает куки одного человека. Ко-локацию задаёт
|
||||||
|
шардирование: события и карта идентичностей раскладываются одним выражением
|
||||||
|
от куки, заказы — от `order_id`. Поэтому обогащение витрин склейкой
|
||||||
|
локально, а сверка и сборка карты ходят через GLOBAL JOIN с обеими
|
||||||
|
сторонами-подзапросами ([исследование
|
||||||
|
соединений](../../research/2026-08-22-distributed-join.md)).
|
||||||
|
|
||||||
|
## Правила слоя
|
||||||
|
|
||||||
|
- DDS обслуживает человека с дашбордом: время лежит в поясе счётчика,
|
||||||
|
пересчёт — один раз при наполнении слоя ([дока хранилища](../storage.md),
|
||||||
|
«Часовые пояса»).
|
||||||
|
- Служебные колонки — координаты собственной загрузки слоя; чужие ярлыки не
|
||||||
|
переносятся, а спрашиваются у их слоя по ключу сущности. Вывод правила и
|
||||||
|
цепочка расследования — [модель заказа](order.md).
|
||||||
|
|
||||||
|
## Открытые решения
|
||||||
|
|
||||||
|
- Сессии, контракт `dds.event_v`, сборка `dds.identity_map` — развилки
|
||||||
|
#131–#133 карты #127.
|
||||||
|
- Запуск трансформаций и механика замены партиций на кластере — #134 и
|
||||||
|
этап 5.
|
||||||
|
- Витрины DM — #135.
|
||||||
|
|
||||||
|
## Родословная
|
||||||
|
|
||||||
|
Дом набора и первая сущность собраны развилкой #130 карты #127; форма
|
||||||
|
набора — [ADR 0011](../../adr/0011-component-docs.md).
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# Модель заказа в 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` (спека, раздел 7). Разбор
|
||||||
|
JSON происходит один раз, на границе ODS → DDS, куда его и положила спека
|
||||||
|
(раздел 2); записывать его параллельными колонками-массивами или `Nested` —
|
||||||
|
выбор DDL, не модели.
|
||||||
|
|
||||||
|
## Движок и наполнение
|
||||||
|
|
||||||
|
`MergeTree` без колонки версии: партиция — `order_date` (день заказа
|
||||||
|
неизменен), ключ сортировки — `order_id`, шардирование распределённой
|
||||||
|
таблицы — `cityHash64(order_id)`, тем же выражением, что у ODS: это
|
||||||
|
единственный устойчивый ключ сущности.
|
||||||
|
|
||||||
|
Идемпотентность держит замена партиции, а не версии. Сбор дня — батч после
|
||||||
|
прихода слепка: трансформация собирает партицию заново из `ods.order_v` и
|
||||||
|
атомарно подменяет. Поэтому каждая строка таблицы — ровно один заказ, и
|
||||||
|
таблица сама служит поверхностью чтения слоя: голый `SELECT`, без `FINAL` и
|
||||||
|
без обёртки-представления. Пока окно K живо, день «дышит» — статусы заказов
|
||||||
|
меняются, и партиция дня пересобирается очередным запуском; на выходе из
|
||||||
|
окна день замерзает.
|
||||||
|
|
||||||
|
Механика запуска — где собирается партиция и как подменяется на кластере —
|
||||||
|
сюда не входит: это устройство трансформаций (#134, этап 5).
|
||||||
|
|
||||||
|
## Служебные колонки
|
||||||
|
|
||||||
|
Строка получает `_load_id` и `_load_ts` запуска трансформации, собравшего
|
||||||
|
её партицию, — по общей конвенции хранилища. При замене партиции они
|
||||||
|
одинаковы у всех строк дня и отвечают на вопрос «каким запуском и когда
|
||||||
|
собран этот день».
|
||||||
|
|
||||||
|
Ярлыки ODS — `snapshot_date`, `_load_id` приёма, `_load_ts` прибытия — в
|
||||||
|
модель не переезжают. Версию заказа они не задают, а физически выжившие их
|
||||||
|
значения — жребий: у строк с равным `updated_at` деловые поля совпадают
|
||||||
|
побайтово, и какая переживёт схлопывание — произвол (замер —
|
||||||
|
[приём заказов](../orders/ingestion.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`, `items_total`, `status`;
|
||||||
|
соединение — GLOBAL JOIN с обеими сторонами-подзапросами
|
||||||
|
([исследование соединений](../../research/2026-08-22-distributed-join.md)).
|
||||||
|
- Сборка `dds.identity_map`: пара `order_id` ↔ `user_id`.
|
||||||
|
|
||||||
|
## Не входит
|
||||||
|
|
||||||
|
- Механика запуска трансформации и замены партиций на кластере — #134 и
|
||||||
|
этап 5.
|
||||||
|
- Контур проверок качества (даг DQ, `dm.dq_summary`) — отдельный разговор
|
||||||
|
этапа 4.
|
||||||
|
- Инкрементальное наполнение вместо пересборки — техдолг #2.
|
||||||
|
|
||||||
|
## Отклонённые варианты
|
||||||
|
|
||||||
|
- **Представление-контракт вместо физической таблицы.** Рамка стенда —
|
||||||
|
показать модель данных, которую хранилище готовит потребителям; урок
|
||||||
|
«слой — контракт, а не копия» уже закреплён за `dds.event_v`, а разбор
|
||||||
|
позиций и пересчёт времён при каждом чтении собирали бы модель заново на
|
||||||
|
лету.
|
||||||
|
- **`ReplacingMergeTree(updated_at)` ещё раз.** Повторил бы механику 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) —
|
||||||
|
цена `FINAL` и представлений; принято картой #127 до этой развилки.
|
||||||
@@ -40,6 +40,8 @@
|
|||||||
`INSERT SELECT` в типизированный ODS и таблицу ошибок; `ods.order`
|
`INSERT SELECT` в типизированный ODS и таблицу ошибок; `ods.order`
|
||||||
принимает версии заказа на `ReplacingMergeTree(updated_at)` —
|
принимает версии заказа на `ReplacingMergeTree(updated_at)` —
|
||||||
[приём из Kafka в ODS](ingestion.md).
|
[приём из Kafka в ODS](ingestion.md).
|
||||||
|
- **Дальше заказ живёт моделью DDS** — уже по хранилищную сторону границы,
|
||||||
|
в наборе слоя: [модель заказа](../dds/order.md).
|
||||||
- **Стартовый мир отправляет семь слепков** (дни 0…6): у последнего прожитого
|
- **Стартовый мир отправляет семь слепков** (дни 0…6): у последнего прожитого
|
||||||
дня клики есть, а заказов нет, и график выручки дозаполняется по ходу
|
дня клики есть, а заказов нет, и график выручки дозаполняется по ходу
|
||||||
мира — [заказы в стартовом мире](start-world.md).
|
мира — [заказы в стартовом мире](start-world.md).
|
||||||
|
|||||||
@@ -129,8 +129,9 @@ JSON-объектом с точным набором ключей: `order_id`, `
|
|||||||
|
|
||||||
Это представление остаётся ответственностью ODS: оно скрывает механику чтения
|
Это представление остаётся ответственностью ODS: оно скрывает механику чтения
|
||||||
версий, но не строит бизнес-модель. DDS читает `ods.order_v` и отдельно решает,
|
версий, но не строит бизнес-модель. DDS читает `ods.order_v` и отдельно решает,
|
||||||
какие сущности, связи и производные признаки ему нужны. Нужен ли `_load_id`
|
какие сущности, связи и производные признаки ему нужны. Судьба служебных
|
||||||
выше ODS, решается вместе с DDS, а не здесь.
|
колонок выше ODS решена вместе с моделью DDS: чужие ярлыки туда не переезжают
|
||||||
|
([модель заказа](../dds/order.md)).
|
||||||
|
|
||||||
## Одно чтение Kafka
|
## Одно чтение Kafka
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ snake_case-имена и расшифровку кодов.
|
|||||||
DDS. `snapshot_date` в нём остаётся датой наблюдения строки, а не ключом
|
DDS. `snapshot_date` в нём остаётся датой наблюдения строки, а не ключом
|
||||||
публикации. Полное решение — в
|
публикации. Полное решение — в
|
||||||
[ADR 0010](../adr/0010-order-versions-in-ods.md) и
|
[ADR 0010](../adr/0010-order-versions-in-ods.md) и
|
||||||
[спецификации приёма заказов](orders/ingestion.md).
|
[спецификации приёма заказов](orders/ingestion.md). Какой моделью заказ
|
||||||
|
живёт дальше — [модель заказа в DDS](dds/order.md).
|
||||||
|
|
||||||
## Часовые пояса
|
## Часовые пояса
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user