From 79728476012dd182719b2b3f3660b38c50e9f374 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Mon, 24 Aug 2026 12:03:26 -0400 Subject: [PATCH 1/2] =?UTF-8?q?docs(dds):=20=D1=81=D0=BF=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D0=B0=20=D0=B2=20DDS=20=D0=B8=20=D0=B7=D0=B0=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D1=91=D0=BD=20=D0=BD=D0=B0=D0=B1=D0=BE=D1=80=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2=20=D1=81=D0=BB=D0=BE=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - развилка #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 --- docs/architecture/dds/README.md | 58 ++++++++++++ docs/architecture/dds/order.md | 127 ++++++++++++++++++++++++++ docs/architecture/orders/README.md | 2 + docs/architecture/orders/ingestion.md | 5 +- docs/architecture/storage.md | 3 +- 5 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 docs/architecture/dds/README.md create mode 100644 docs/architecture/dds/order.md diff --git a/docs/architecture/dds/README.md b/docs/architecture/dds/README.md new file mode 100644 index 0000000..bca2b70 --- /dev/null +++ b/docs/architecture/dds/README.md @@ -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). diff --git a/docs/architecture/dds/order.md b/docs/architecture/dds/order.md new file mode 100644 index 0000000..1af14a6 --- /dev/null +++ b/docs/architecture/dds/order.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 до этой развилки. diff --git a/docs/architecture/orders/README.md b/docs/architecture/orders/README.md index 1f7c9ef..97d67ef 100644 --- a/docs/architecture/orders/README.md +++ b/docs/architecture/orders/README.md @@ -40,6 +40,8 @@ `INSERT SELECT` в типизированный ODS и таблицу ошибок; `ods.order` принимает версии заказа на `ReplacingMergeTree(updated_at)` — [приём из Kafka в ODS](ingestion.md). +- **Дальше заказ живёт моделью DDS** — уже по хранилищную сторону границы, + в наборе слоя: [модель заказа](../dds/order.md). - **Стартовый мир отправляет семь слепков** (дни 0…6): у последнего прожитого дня клики есть, а заказов нет, и график выручки дозаполняется по ходу мира — [заказы в стартовом мире](start-world.md). diff --git a/docs/architecture/orders/ingestion.md b/docs/architecture/orders/ingestion.md index 39b9e5a..2d4edd0 100644 --- a/docs/architecture/orders/ingestion.md +++ b/docs/architecture/orders/ingestion.md @@ -129,8 +129,9 @@ JSON-объектом с точным набором ключей: `order_id`, ` Это представление остаётся ответственностью ODS: оно скрывает механику чтения версий, но не строит бизнес-модель. DDS читает `ods.order_v` и отдельно решает, -какие сущности, связи и производные признаки ему нужны. Нужен ли `_load_id` -выше ODS, решается вместе с DDS, а не здесь. +какие сущности, связи и производные признаки ему нужны. Судьба служебных +колонок выше ODS решена вместе с моделью DDS: чужие ярлыки туда не переезжают +([модель заказа](../dds/order.md)). ## Одно чтение Kafka diff --git a/docs/architecture/storage.md b/docs/architecture/storage.md index 1ab42e9..c11c480 100644 --- a/docs/architecture/storage.md +++ b/docs/architecture/storage.md @@ -207,7 +207,8 @@ snake_case-имена и расшифровку кодов. DDS. `snapshot_date` в нём остаётся датой наблюдения строки, а не ключом публикации. Полное решение — в [ADR 0010](../adr/0010-order-versions-in-ods.md) и -[спецификации приёма заказов](orders/ingestion.md). +[спецификации приёма заказов](orders/ingestion.md). Какой моделью заказ +живёт дальше — [модель заказа в DDS](dds/order.md). ## Часовые пояса -- 2.54.0 From 40de2e5c5fff525fcee481e092f216edbe6a7fd1 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Mon, 24 Aug 2026 12:06:54 -0400 Subject: [PATCH 2/2] =?UTF-8?q?docs(dds):=20=D0=B2=D1=8B=D1=87=D0=B8=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D1=8B=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=B0=20=D0=B8=20=D0=BA=D0=B0?= =?UTF-8?q?=D1=80=D1=82=D0=B0=20=D1=81=D0=BB=D0=BE=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - горячее ревью после написания: неточности формулировок и один пропуск в списке читаемых колонок. - Что: - уточнено, что дом набора выбран развилкой #130, а не «первой развилкой карты» (первыми шли research-тикеты); - сверке добавлен order_date в перечень читаемых колонок (спека, §7: колонка order_day); - мастер-спека слинкована при первом упоминании; пояснена роль резолюции #128 в «Связанных решениях». - Проверка: - только документы; ссылки сверены с деревом docs/. Co-Authored-By: Claude Fable 5 --- docs/architecture/dds/README.md | 5 +++-- docs/architecture/dds/order.md | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/architecture/dds/README.md b/docs/architecture/dds/README.md index bca2b70..1da3e8f 100644 --- a/docs/architecture/dds/README.md +++ b/docs/architecture/dds/README.md @@ -7,8 +7,9 @@ Рамки заданы мастер-спекой [«Боевой реализм стенда (v2)»](../../specs/2026-07-30-stand-v2-realism.md): карта таблиц и витрины — раздел 7, сверка — раздел 4, склейка идентичностей — раздел 5. Дом выбран -на первой развилке карты: резолюции этапа 4 живут здесь, по хранилищную -сторону границы «источник | хранилище». Устройство источников остаётся их +развилкой #130 — первой, которой понадобилось записать резолюцию в доки: +записи этапа 4 живут здесь, по хранилищную сторону границы +«источник | хранилище». Устройство источников остаётся их собственным наборам ([заказы бэкенда](../orders/README.md)) — генератор переносим и о нашем DDS не знает. diff --git a/docs/architecture/dds/order.md b/docs/architecture/dds/order.md index 1af14a6..3d65bc9 100644 --- a/docs/architecture/dds/order.md +++ b/docs/architecture/dds/order.md @@ -30,7 +30,8 @@ K живёт на текущем состоянии само: пересборк одной длины — артикул, количество, цена (`Decimal`). Это родная для ClickHouse денормализация: позиция не живёт без заказа и собственного ключа не имеет, а разворачивает массивы читатель — `ARRAY JOIN` в витрине -выручки, с обогащением категорией через `dictGet` (спека, раздел 7). Разбор +выручки, с обогащением категорией через `dictGet` +([мастер-спека](../../specs/2026-07-30-stand-v2-realism.md), раздел 7). Разбор JSON происходит один раз, на границе ODS → DDS, куда его и положила спека (раздел 2); записывать его параллельными колонками-массивами или `Nested` — выбор DDL, не модели. @@ -82,8 +83,8 @@ JSON происходит один раз, на границе ODS → DDS, ку - Витрина выручки `dm.revenue_daily_v`: заказы в `paid`, `ARRAY JOIN` позиций, категория через `dictGet` (спека, раздел 7). -- Сверка `dm.purchase_vs_orders_v`: `order_id`, `items_total`, `status`; - соединение — GLOBAL JOIN с обеими сторонами-подзапросами +- Сверка `dm.purchase_vs_orders_v`: `order_id`, `order_date`, `items_total`, + `status`; соединение — GLOBAL JOIN с обеими сторонами-подзапросами ([исследование соединений](../../research/2026-08-22-distributed-join.md)). - Сборка `dds.identity_map`: пара `order_id` ↔ `user_id`. @@ -124,4 +125,5 @@ JSON происходит один раз, на границе ODS → DDS, ку четыре координаты строки ODS. - [Проталкивание условий сквозь представления](../../research/2026-08-22-view-predicate-pushdown.md) — - цена `FINAL` и представлений; принято картой #127 до этой развилки. + резолюция #128 той же карты: цена `FINAL` и цепочек представлений, фон, на + котором здесь выбрана физическая таблица. -- 2.54.0