# Карта идентичностей в 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` нет. Запланированные витрины его не читают, а два возможных смысла — «первая покупка вообще» и «момент, с которого связь была известна хранилищу» — требуют разных исторических моделей. Этот вопрос оставлен [самостоятельной работе](../../labs/optional/identity-over-time.md), а текущий стенд не выдаёт один смысл за другой. Статус заказа связь не меняет: любой заказ, сопоставленный с событием покупки, показывает владельца куки. Отмена меняет состояние заказа, но не личность покупателя. ## Физическая модель и публичный интерфейс - `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` между разными ключами шардирования.