docs(dds): зафиксирована физическая модель карты идентичностей

- Зачем:
  - согласованная модель нужна для пакетной реализации DDS и понятного публичного интерфейса слоя.
- Что:
  - описаны физическая карта идентичностей, полный пересчёт и отказ от first_matched_at.
  - унифицирован публичный контракт _v для событий, заказов, сессий и карты.
  - синхронизированы мастер-спека, имена объектов и граница доступа.
- Проверка:
  - git diff --cached --check.
This commit is contained in:
ddadmin committed 2026-08-24 15:52:17 -04:00
1 parent 256d51840b
commit f0c4b4c627
7 files changed
+234 -59

No files matched your search

+32 -18
View File
@@ -224,14 +224,15 @@ Ecommerce (заполнены только у торговых событий):
неизменного `created_at`, шардирование по `cityHash64(order_id)`.
`snapshot_date` остаётся датой наблюдения источника, но не задаёт публикацию
или идемпотентность. Физическое чтение может видеть несколько версий;
`ods.order_v` возвращает точное текущее состояние. Устройство модели DDS и
способ её материализации решаются отдельно.
`ods.order_v` возвращает точное текущее состояние. В DDS одна строка хранит
один заказ с позициями в типизированных массивах; физическая модель и
пакетное наполнение описаны в [доке заказа](../architecture/dds/order.md).
Пропущенный день ничего не ломает, следующий слепок самовосстанавливает.
- `items` остаётся сырой строкой JSON в ODS. Разбирать позиции следует на
границе ODS → DDS, но их представление определяется вместе с будущей моделью
заказов. Это остаётся носителем навыка «вложенный JSON в ClickHouse», не
превращая приём в преждевременную модель данных.
- `items` остаётся сырой строкой JSON в ODS. Позиции разбираются на границе
ODS → DDS в типизированные массивы строки заказа. Это остаётся носителем
навыка «вложенный JSON в ClickHouse», не превращая приём в преждевременную
модель данных.
- Статусы держим все три: смена `created` → `paid` и есть причина «дыхания»
выручки внутри окна; сужение до двух — резервный срез 1. На выходе из окна
заказ либо `paid`, либо `cancelled`: неоплаченного отменяют, «навсегда
@@ -292,12 +293,19 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
- **Карта соответствий кука↔пользователь** строится трансформацией из уже
существующего моста: `purchase`-событие (`ClientID`, `purchaseID`) ↔ заказ
(`order_id`, `user_id`). Ни новых полей, ни нового транспорта.
- Форма и место карты: таблица `dds.identity_map`
(`client_id` UInt64, `user_id` UInt64, `first_matched_at` DateTime),
ReplacingMergeTree, `ORDER BY (client_id, user_id)`, шардирование по
`cityHash64(client_id)` — тем же выражением, что события (иначе ко-локации
нет); ко-локация делает обогащение витрин локальным;
заказы при сборке карты подтягиваются через GLOBAL JOIN.
- Форма и место карты: `dds.identity_map_rep` хранит пару `client_id` UInt64,
`user_id` UInt64 и координаты собственной загрузки `_load_id`, `_load_ts` в
`ReplicatedReplacingMergeTree(_load_ts)` с `ORDER BY (client_id, user_id)`.
`dds.identity_map_dist` шардирует по `cityHash64(client_id)` — тем же
выражением, что события; ко-локация позволяет обогащать витрины локально.
Публичный интерфейс `dds.identity_map_v` перечисляет колонки явно и прячет
`FINAL`. `first_matched_at` в стенде нет: у него нет читателя; история
склейки оставлена самостоятельной работе в issue #145.
- Карта пересобирается пакетно после модели заказов: все `purchase` из
`dds.event_v` соединяются с текущими заказами `dds.order_v` через
`GLOBAL JOIN`, статус заказа связь покупателя с кукой не отменяет. Каждый
запуск пишет полный текущий набор различных пар; предыдущий результат не
служит источником следующего.
- **N:1**: часть покупателей покупает с двух кук («телефон и ноутбук») —
параметр мира; значение фиксирует эта спека: 15%, детерминировано
от seed. Ядро лабы:
@@ -422,16 +430,20 @@ README.
| STG | `stg.orders_raw_kafka`, `stg.orders_raw`, без MV | сырые строки слепка; чтец на ноде 1, забирает пакетный шаг |
| ODS | `ods.event` (+`_errors`), `ods.event_v` | типизированные версии событий, брак и точное актуальное состояние |
| ODS | `ods.order` (+`_errors`), `ods.order_v` | типизированные версии заказов, брак и точное текущее состояние |
| DDS | `dds.session` | сборка сессий из событий (наследник `dds.click`) |
| DDS | `dds.session_rep` / `_dist`, `dds.session_v` | физическая сборка сессий и её публичный интерфейс |
| DDS | `dds.event_v` | представление над `ods.event`: snake_case-имена, расшифровка кодов `DeviceCategory`; витрины DM читают его, а не ODS напрямую |
| DDS | модель заказов | зерно, связи и материализация проектируются на этапе DDS |
| DDS | `dds.identity_map` | карта кука↔пользователь |
| DDS | `dds.order_rep` / `_dist`, `dds.order_v` | текущая модель заказа и её публичный интерфейс |
| DDS | `dds.identity_map_rep` / `_dist`, `dds.identity_map_v` | физическая карта кука↔пользователь и её точный публичный интерфейс |
| DIC | подложка `products_file` и словарь `products` | каталог из CSV, вне цепочки слоёв ([ADR 0012](../adr/0012-dictionary-home.md)) |
| DM | витрины `dm.*_v`, `dm.dq_summary` | см. ниже |
У каждой таблицы слоя — пара из локальной и распределённой, имена по конвенции
суффиксов; она же задаёт служебные колонки, нарезку и срок хранения сырья —
см. [доку хранилища](../architecture/storage.md).
Потребительские роли и витрины читают сущности DDS через `_v` с явным списком
колонок; прав на `_rep` и `_dist` у них нет. Физические объекты остаются ролям
загрузки, диагностики и обслуживания. Механизм и проверяемая граница доступа —
в [доке хранилища](../architecture/storage.md), «Имена объектов».
Заказы принимаются **пакетным забором** ([ADR
0008](../adr/0008-order-ingestion.md), [ADR
@@ -494,7 +506,7 @@ Kafka день переигрывается генератором заново:
счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется
`declared_revenue` по UTM.
- **`daily_traffic_v`** — расширяется парой «посетители» / «известные
пользователи» (обогащение через `dds.identity_map`, локальное соединение
пользователи» (обогащение через `dds.identity_map_v`, локальное соединение
по ключу ко-локации).
- `events_enriched_v`, `top_pages_daily_v`, `session_overview_v`,
`dq_errors_daily_v` — переезжают на новую модель без смены роли: источник —
@@ -670,8 +682,10 @@ Kafka Engine на двух нодах снят с этого списка при
`ReplacingMergeTree`. Проверять то, что заведомо случается и заведомо
обезврежено, — работа без ответа на конце.
- Поведение соединения двух Distributed-таблиц и `distributed_product_mode` —
эмпирически на стенде (хвост #14).
- Точную форму локального обогащения `daily_traffic_v` через публичные `_v` —
эмпирически при проектировании витрин #135. Общее поведение соединения двух
Distributed-таблиц уже проверено в
[исследовании](../research/2026-08-22-distributed-join.md).
- Размер артефакта эталонного мира после пересборки.
- Спорные API (Airflow Datasets/сенсоры, ClickHouse DDL) — перед кодом
сверять через MCP Context7 (правило AGENTS.md).