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

+122
View File
@@ -0,0 +1,122 @@
# Карта идентичностей в DDS
Учебный результат: менти строит физический мост между кукой трекера и
пользователем магазина, раскладывает его по ключу будущего соединения и видит,
как пакетный полный пересчёт сочетается с точным публичным представлением.
## Назначение
Карта — адресная книга: «эта кука уже покупала как этот пользователь
магазина». Связь становится известна только после покупки, потому что в
кликстриме есть `client_id`, а `user_id` живёт в заказе. Две куки одного
покупателя дают две строки с одним `user_id`; поэтому витрина может показать
рядом число посетителей по кукам и число известных людей после склейки.
Это не полная клиентская база. Куки без покупки остаются анонимными, а карта не
пытается угадать их владельца.
## Зерно и состав строки
Одна строка — известная пара `client_id` × `user_id`. Деловых колонок две:
- `client_id UInt64` — кука трекера;
- `user_id UInt64` — пользователь магазина.
Собственные служебные колонки `_load_id String` и
`_load_ts DateTime64(3, 'UTC')` показывают, каким запуском и когда пара записана
в DDS. Они имеют читателя при расследовании повтора и потому входят в публичный
договор вместе с деловой парой.
`first_matched_at` нет. Запланированные витрины его не читают, а два возможных
смысла — «первая покупка вообще» и «момент, с которого связь была известна
хранилищу» — требуют разных исторических моделей. Этот вопрос оставлен
[самостоятельной работе](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/145),
а текущий стенд не выдаёт один смысл за другой.
Статус заказа связь не меняет: любой заказ, сопоставленный с событием покупки,
показывает владельца куки. Отмена меняет состояние заказа, но не личность
покупателя.
## Физическая модель и публичный интерфейс
- `dds.identity_map_rep` — локальная
`ReplicatedReplacingMergeTree(_load_ts)`, `ORDER BY (client_id, user_id)`,
без партиционирования;
- `dds.identity_map_dist` — распределённая поверхность записи, шардирование
по `cityHash64(client_id)`;
- `dds.identity_map_v` — публичное обычное представление над `_dist`, которое
перечисляет четыре колонки явно и применяет `FINAL`.
Повторы полного запуска физически могут лежать рядом до фонового слияния.
`FINAL` оставляет одну последнюю версию пары, поэтому пользователь всегда читает
точную карту через `_v`. Роли объектов и доступ следуют общей
[конвенции](../storage.md), «Имена объектов».
Шардирование совпадает с событиями и сессиями. Благодаря этому витрина может
обогащать их картой локально по `client_id`; точную форму распределённого
запроса решает проектирование витрин #135.
## Сборка
Карта собирается один раз в суточной пакетной цепочке после обновления модели
заказов и до зависимых витрин. При догоняющем запуске за несколько дней хватает
одной сборки после всей пачки заказов.
Источники — вся доступная история `dds.event_v` и текущее состояние заказов
`dds.order_v`. Трансформация:
1. оставляет события `purchase` и разворачивает единственный `purchase_id`;
2. соединяет его с `order_id` заказа через `GLOBAL INNER JOIN`;
3. выбирает различные пары `client_id`, `user_id`;
4. добавляет координаты текущего запуска и пишет полный набор в
`dds.identity_map_dist`.
`GLOBAL` нужен потому, что события разложены по `client_id`, а заказы — по
`order_id`: строки соединения не ко-локированы. Правая сторона мала, поэтому
рассылка заказов приемлема. Материализованное представление здесь не подходит:
заказ может появиться в пакетном слепке позже уже принятого события покупки.
## Повтор запуска
Каждый запуск заново получает все известные пары из источников. Он не читает
предыдущую карту и не делает anti-join «вставить только отсутствующее».
`_load_ts` служит версией `ReplacingMergeTree`, а `dds.identity_map_v` прячет
физические повторы через `FINAL`.
Полный пересчёт выбран потому, что карта стенда мала, источники уже содержат
нужную историю, а повтор задачи не зависит от того, на каком месте оборвался
предыдущий. Мир стенда не отзывает связь куки с пользователем, поэтому удалять
исчезнувшие пары не требуется.
## Читатели
- `dm.daily_traffic_v` — посетители по `client_id` и известные пользователи по
`user_id` рядом;
- учебные запросы — показать, как несколько кук одного человека меняют
`uniq`;
- расследование пакетного запуска — `_load_id` и `_load_ts` текущей версии
пары.
## Отклонённые варианты
- **`first_matched_at` в текущей модели.** Нет запланированного читателя и нет
однозначной временной семантики; идея передана в issue #145.
- **Словарь ClickHouse.** Для малой карты мог бы ускорить точечное обогащение,
но спрятал бы физическую модель и не добавил нового урока рядом с уже
существующим словарём товаров.
- **Одно динамическое представление без физической карты.** Повторяло бы
соединение всей истории при каждом чтении, не давало ко-локации результата и
не показывало устройство реального слоя хранения.
- **Инкремент через anti-join с самой картой.** Делал бы результат предыдущего
запуска обязательным входом и усложнял повтор без пользы на объёме стенда.
- **Материализованное представление от событий.** Не увидело бы заказ, который
приехал позже события покупки.
## Связанные решения
- [Карта DDS](README.md) — связи сущностей и публичная поверхность `_v`.
- [Модель заказа](order.md) — текущее состояние заказа и пакетное наполнение.
- [Контракт события](event.md) — поля событий на языке DDS.
- [Исследование распределённых соединений](../../research/2026-08-22-distributed-join.md)
— форма `GLOBAL JOIN` и капкан `INSERT SELECT` между разными ключами
шардирования.