docs(dds): зафиксирована физическая модель карты идентичностей
- Зачем: - согласованная модель нужна для пакетной реализации DDS и понятного публичного интерфейса слоя. - Что: - описаны физическая карта идентичностей, полный пересчёт и отказ от first_matched_at. - унифицирован публичный контракт _v для событий, заказов, сессий и карты. - синхронизированы мастер-спека, имена объектов и граница доступа. - Проверка: - git diff --cached --check.
This commit is contained in:
1 parent
256d51840b
commit
f0c4b4c627
7 files changed
+234
-59
No files matched your search
@@ -18,16 +18,18 @@
|
||||
Целостность модели — связи и ко-локация — живёт в этом разделе; файлы
|
||||
сущностей держат глубину и на карту ссылаются.
|
||||
|
||||
- **Заказ** (`dds.order`) — физическая сущность: одна строка — заказ в
|
||||
текущем состоянии, позиции массивами в строке, наполняется заменой
|
||||
партиций — [модель заказа](order.md).
|
||||
- **Сессия** (`dds.session`) — расчётная сущность: одна строка — визит,
|
||||
- **Заказ** (`dds.order_v`, хранение `dds.order_rep` / `_dist`) — физическая
|
||||
сущность: одна строка — заказ в текущем состоянии, позиции массивами в
|
||||
строке, наполняется заменой партиций — [модель заказа](order.md).
|
||||
- **Сессия** (`dds.session_v`, хранение `dds.session_rep` / `_dist`) —
|
||||
расчётная сущность: одна строка — визит,
|
||||
нарезку по правилу визита стенд делает сам, `VisitID` генератора — эталон
|
||||
сверки; наполнение заменой дневной партиции — [модель сессии](session.md).
|
||||
- **Событие** (`dds.event_v`) — контракт над ODS, а не копия: представление,
|
||||
переводящее событие на язык бизнес-модели, — [контракт события](event.md).
|
||||
- **Карта идентичностей** (`dds.identity_map`) — кука ↔ пользователь
|
||||
магазина; сборку решает развилка #133.
|
||||
- **Карта идентичностей** (`dds.identity_map_v`, хранение
|
||||
`dds.identity_map_rep` / `_dist`) — кука ↔ пользователь магазина —
|
||||
[модель карты](identity.md).
|
||||
|
||||
Связи держатся двумя ключами моста (спека, разделы 4 и 5): `purchaseID[1]`
|
||||
события равен `order_id` заказа — по нему идут сверка и сборка склейки;
|
||||
@@ -43,13 +45,15 @@
|
||||
- DDS обслуживает человека с дашбордом: время лежит в поясе счётчика,
|
||||
пересчёт — один раз при наполнении слоя ([дока хранилища](../storage.md),
|
||||
«Часовые пояса»).
|
||||
- Публичный договор каждой сущности — `_v`; роли объектов и проверяемая
|
||||
граница доступа заданы [конвенциями хранилища](../storage.md), «Имена
|
||||
объектов».
|
||||
- Служебные колонки — координаты собственной загрузки слоя; чужие ярлыки не
|
||||
переносятся, а спрашиваются у их слоя по ключу сущности. Вывод правила и
|
||||
цепочка расследования — [модель заказа](order.md).
|
||||
|
||||
## Открытые решения
|
||||
|
||||
- Сборка `dds.identity_map` — развилка #133 карты #127.
|
||||
- Запуск трансформаций и механика замены партиций на кластере — #134 и
|
||||
этап 5.
|
||||
- Витрины DM — #135.
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
индекс перестал бы её анализировать. Цена `FINAL` — отключённый
|
||||
автоматический `PREWHERE` — свойство ODS, представление её не усугубляет.
|
||||
|
||||
Как публичный договор DDS, представление перечисляет колонки явно и следует
|
||||
общей [границе доступа](../storage.md), «Имена объектов».
|
||||
|
||||
## Язык модели
|
||||
|
||||
Имена атрибутов — имена модели, а не нормализованные имена источника
|
||||
@@ -38,7 +41,7 @@
|
||||
выбор линзы слой сделал за читателя, UTC остаётся языком ODS.
|
||||
|
||||
`ClientID` остаётся `client_id`: это устоявшееся имя куки во всей
|
||||
экосистеме Метрики, по нему шардирование и мост к `dds.identity_map`;
|
||||
экосистеме Метрики, по нему шардирование и мост к `dds.identity_map_v`;
|
||||
своё имя здесь создало бы третий синоним. `client_timezone` тоже остаётся —
|
||||
это атрибут посетителя, а не линза времени.
|
||||
|
||||
@@ -79,12 +82,13 @@ issue #143.
|
||||
|
||||
## Читатели
|
||||
|
||||
- Сборка `dds.session`: ключи нарезки (`client_id`, `event_time`,
|
||||
- Сборка сессий: ключи нарезки (`client_id`, `event_time`,
|
||||
`event_date`), тип события, страница, UTM, паспорт куки — категория
|
||||
устройства приходит уже расшифрованной.
|
||||
- Витрины `dm.events_enriched_v`, `dm.top_pages_daily_v`,
|
||||
`dm.utm_effectiveness_v` (`declared_revenue` — из `purchase_revenue`),
|
||||
`dm.daily_traffic_v` (пара «посетители / известные» через `identity_map`).
|
||||
`dm.daily_traffic_v` (пара «посетители / известные» через
|
||||
`dds.identity_map_v`).
|
||||
- Событийная сторона сверки `dm.purchase_vs_orders_v`: `purchase_id`,
|
||||
`purchase_revenue`, `event_date`.
|
||||
- DQ-сверка сессий с эталоном читает не его, а `ods.event_v` —
|
||||
|
||||
@@ -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` между разными ключами
|
||||
шардирования.
|
||||
@@ -38,18 +38,22 @@ JSON происходит один раз, на границе ODS → DDS, ку
|
||||
|
||||
## Движок и наполнение
|
||||
|
||||
`MergeTree` без колонки версии: партиция — `order_date` (день заказа
|
||||
неизменен), ключ сортировки — `order_id`, шардирование распределённой
|
||||
таблицы — `cityHash64(order_id)`, тем же выражением, что у ODS: это
|
||||
единственный устойчивый ключ сущности.
|
||||
`dds.order_rep` — `ReplicatedMergeTree` без колонки версии: партиция —
|
||||
`order_date` (день заказа неизменен), ключ сортировки — `order_id`.
|
||||
`dds.order_dist` шардирует по `cityHash64(order_id)`, тем же выражением, что
|
||||
ODS: это единственный устойчивый ключ сущности.
|
||||
|
||||
Идемпотентность держит замена партиции, а не версии. Сбор дня — батч после
|
||||
прихода слепка: трансформация собирает партицию заново из `ods.order_v` и
|
||||
атомарно подменяет. Поэтому каждая строка таблицы — ровно один заказ, и
|
||||
таблица сама служит поверхностью чтения слоя: голый `SELECT`, без `FINAL` и
|
||||
без обёртки-представления. Пока окно K живо, день «дышит» — статусы заказов
|
||||
меняются, и партиция дня пересобирается очередным запуском; на выходе из
|
||||
окна день замерзает.
|
||||
атомарно подменяет. Поэтому каждая строка физической таблицы — ровно один
|
||||
заказ, а чтению не нужен `FINAL`. Пока окно K живо, день «дышит» — статусы
|
||||
заказов меняются, и партиция дня пересобирается очередным запуском; на выходе
|
||||
из окна день замерзает.
|
||||
|
||||
Публичная поверхность — `dds.order_v`: обычное представление с явным списком
|
||||
деловых и собственных служебных колонок над `dds.order_dist`, без `FINAL`.
|
||||
Роли объектов и доступ следуют общей [конвенции](../storage.md), «Имена
|
||||
объектов».
|
||||
|
||||
Механика запуска — где собирается партиция и как подменяется на кластере —
|
||||
сюда не входит: это устройство трансформаций (#134, этап 5).
|
||||
@@ -86,7 +90,8 @@ JSON происходит один раз, на границе ODS → DDS, ку
|
||||
- Сверка `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`.
|
||||
- Сборка карты идентичностей: пара `order_id` ↔ `user_id`, источник —
|
||||
`dds.order_v`.
|
||||
|
||||
## Не входит
|
||||
|
||||
@@ -98,11 +103,12 @@ JSON происходит один раз, на границе ODS → DDS, ку
|
||||
|
||||
## Отклонённые варианты
|
||||
|
||||
- **Представление-контракт вместо физической таблицы.** Рамка стенда —
|
||||
- **Одно динамическое представление вместо физической таблицы.** Рамка стенда —
|
||||
показать модель данных, которую хранилище готовит потребителям; урок
|
||||
«слой — контракт, а не копия» уже закреплён за `dds.event_v`, а разбор
|
||||
позиций и пересчёт времён при каждом чтении собирали бы модель заново на
|
||||
лету.
|
||||
лету. Публичное `dds.order_v` поверх готовой физической модели этому доводу
|
||||
не противоречит: оно ничего заново не вычисляет.
|
||||
- **`ReplacingMergeTree(updated_at)` ещё раз.** Повторил бы механику ODS
|
||||
вторым разом: физически хранил бы устаревшие состояния, вернул бы `FINAL`
|
||||
и обёртку-представление — а урок версий менти уже отыграл.
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
свою нарезку с эталоном. Стенд здесь играет роль сессионизатора: в бою её
|
||||
играет Метрика, которая режет визиты у себя и с октября 2025 отдаёт готовый
|
||||
`VisitID` прямо в хитах. Поэтому стенд считает сессии сам, а `VisitID`
|
||||
генератора использует только как эталон самопроверки — иначе сборка
|
||||
`dds.session` выродилась бы в `GROUP BY` по чужому полю и центральный урок
|
||||
генератора использует только как эталон самопроверки — иначе сборка сессий
|
||||
выродилась бы в `GROUP BY` по чужому полю и центральный урок
|
||||
слоя пропал бы. Без этого абзаца действия стенда выглядят странно: «зачем
|
||||
считать то, что уже дано колонкой».
|
||||
|
||||
@@ -35,10 +35,10 @@
|
||||
ломают.
|
||||
|
||||
`session_id` — не ключ сортировки и не ключ шардирования, а колонка-ручка:
|
||||
витрины сессий читают таблицу напрямую, их зерно совпадает с зерном модели,
|
||||
и в соединениях id не участвует — события его не несут, мост к заказам живёт
|
||||
на событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со
|
||||
стендовой и назвать расхождение одним числом) и разбор DQ-расхождений.
|
||||
витрины сессий читают `dds.session_v`, их зерно совпадает с зерном модели, и в
|
||||
соединениях id не участвует — события его не несут, мост к заказам живёт на
|
||||
событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со стендовой и
|
||||
назвать расхождение одним числом) и разбор DQ-расхождений.
|
||||
|
||||
## Состав строки
|
||||
|
||||
@@ -64,10 +64,11 @@
|
||||
|
||||
## Движок и наполнение
|
||||
|
||||
`MergeTree` без колонки версии, по образцу заказа: партиция — день сессии,
|
||||
ключ сортировки — кука и начало сессии, шардирование распределённой таблицы —
|
||||
`cityHash64(ClientID)`, тем же выражением, что у событий, — ко-локация с
|
||||
`dds.identity_map` ([дока хранилища](../storage.md), «Раскладка по шардам»).
|
||||
`dds.session_rep` — `ReplicatedMergeTree` без колонки версии, по образцу
|
||||
заказа: партиция — день сессии, ключ сортировки — кука и начало сессии.
|
||||
`dds.session_dist` шардирует по `cityHash64(client_id)`, тем же выражением,
|
||||
что события, — это ко-локация с `dds.identity_map_rep` ([дока
|
||||
хранилища](../storage.md), «Раскладка по шардам»).
|
||||
Совпадение ключа с событиями не случайно: все события куки лежат на одном
|
||||
шарде, поэтому сессионизация локальна по природе — капкан смены ключа при
|
||||
вставке, который дока хранилища обещает сборкам DDS, у сессий не стреляет.
|
||||
@@ -78,10 +79,15 @@
|
||||
целиком; идемпотентность держит замена партиции. В отличие от заказов, где
|
||||
батч пересобирает до K «дышащих» дней, у сессий пересобирается только одна
|
||||
партиция: день самодостаточен и замерзает сразу (правило спеки, раздел 6).
|
||||
Внутри живого дня `dds.session` про сегодня честно молчит — «сегодня»
|
||||
Внутри живого дня модель сессии про сегодня честно молчит — «сегодня»
|
||||
смотрят через `dds.event_v`; сессия и по смыслу не финальна, пока день не
|
||||
прожит.
|
||||
|
||||
Публичная поверхность — `dds.session_v`: обычное представление с явным
|
||||
списком деловых и собственных служебных колонок над `dds.session_dist`.
|
||||
Роли объектов и доступ следуют общей [конвенции](../storage.md), «Имена
|
||||
объектов».
|
||||
|
||||
Источник сборки — `dds.event_v` ([контракт события](event.md)): сессии
|
||||
нужны от него ключи нарезки, тип события, страница, UTM и паспорт куки.
|
||||
Механика запуска — где собирается партиция и как подменяется на кластере —
|
||||
@@ -102,7 +108,7 @@
|
||||
## Читатели
|
||||
|
||||
- Витрина обзора сессий `session_overview_v`: длительности, глубина,
|
||||
вход/выход; читает таблицу напрямую, зерно совпадает (#135).
|
||||
вход/выход; читает `dds.session_v`, зерно совпадает (#135).
|
||||
- DQ-сверка с эталоном (см. выше).
|
||||
- Лаба сессий: менти собирает нарезку сам до знакомства с кодом стенда,
|
||||
потом сравнивает — с эталоном и со стендовой.
|
||||
@@ -124,9 +130,11 @@
|
||||
- **Гибрид: сборка по `VisitID`, DQ пересчитывает нарезку.** Сторож на
|
||||
нарезку, которой сборка не делает; та же работа, что в варианте «считать
|
||||
сам», но урок спрятан в проверку.
|
||||
- **Представление вместо таблицы.** Оконная сессионизация на каждое чтение;
|
||||
- **Одно динамическое представление вместо физической таблицы.** Оконная
|
||||
сессионизация на каждое чтение;
|
||||
урок «слой — контракт, а не копия» уже закреплён за `dds.event_v` — доводы
|
||||
модели заказа, здесь они только сильнее.
|
||||
модели заказа, здесь они только сильнее. Публичное `dds.session_v` поверх
|
||||
готовой физической модели ничего заново не считает.
|
||||
- **Счётчик или UUID вместо детерминированного хеша.** Пересборка партиции
|
||||
выдала бы другие id — замена перестала бы быть идемпотентной.
|
||||
- **`VisitID` колонкой в строке сессии.** Чужой чекан в своей модели: наша
|
||||
|
||||
@@ -36,7 +36,7 @@ CSV-файла. Живёт он вне цепочки слоёв, в зоне `d
|
||||
| `_kafka` | таблица на движке `Kafka` |
|
||||
| `_file` | таблица на движке `File` — чтец файла |
|
||||
| `_mv` | материализованное представление |
|
||||
| `_v` | обычное представление |
|
||||
| `_v` | обычное представление, публичная поверхность чтения |
|
||||
|
||||
Суффикс носит каждый физический объект. Голого имени у таблицы не существует:
|
||||
запрос к `stg.hits_raw` даёт громкую ошибку «нет такой таблицы» — а под голым
|
||||
@@ -63,6 +63,23 @@ CSV-файла. Живёт он вне цепочки слоёв, в зоне `d
|
||||
`order`, `session`. Граница между «как привезли» и «что это такое»
|
||||
проходит по STG, и имена её показывают.
|
||||
|
||||
В DDS публичный договор сущности заканчивается на `_v`. Потребительские запросы
|
||||
и витрины называют только представление; `_rep` и `_dist` остаются ролям
|
||||
загрузки, диагностики и обслуживания. Представление перечисляет колонки явно,
|
||||
даже если его запрос — простая проекция над `_dist`: договор не выдаёт
|
||||
устройство кластера и случайно добавленные физической таблице колонки. У
|
||||
события физической копии в DDS нет, но правило то же: `dds.event_v` — его
|
||||
публичный договор.
|
||||
|
||||
Граница доступа — часть договора, а не следствие имени. Обычное представление
|
||||
создаётся с явными `DEFINER = CURRENT_USER SQL SECURITY DEFINER`: определивший
|
||||
его пользователь читает источники, потребительская роль получает `SELECT` на
|
||||
`_v`, физические объекты — роль загрузки или диагностики. При стандартном
|
||||
`INVOKER` потребителю нужны были бы права и на источник ([документация
|
||||
ClickHouse](https://clickhouse.com/docs/sql-reference/statements/create/view#sql-security)).
|
||||
Значит реализация публичной поверхности закончена, когда запрос
|
||||
потребительской ролью проходит через `_v` и отклоняется для `_rep` и `_dist`.
|
||||
|
||||
## Раскладка по шардам
|
||||
|
||||
Пишем только в `_dist`. Локальные таблицы остаются для чтения и обслуживания —
|
||||
@@ -98,10 +115,10 @@ FROM источник_dist`. Заводское значение `parallel_distr
|
||||
|
||||
Ключи ко-локации названы заранее, потому что на них стоит политика соединений из
|
||||
раздела 6 спеки: обычное соединение разрешено только по ключу ко-локации, всё
|
||||
прочее — через `GLOBAL`. Значит `dds.session` и `dds.identity_map` шардируются по
|
||||
`cityHash64(ClientID)`. Объекты DDS с зерном заказа должны сохранять ко-локацию
|
||||
по `cityHash64(order_id)`; ключи остальных частей будущей модели и витрин
|
||||
появятся вместе с ними.
|
||||
прочее — через `GLOBAL`. Значит `dds.session_dist` и `dds.identity_map_dist`
|
||||
шардируются по `cityHash64(client_id)`. Объекты DDS с зерном заказа сохраняют
|
||||
ко-локацию по `cityHash64(order_id)`; ключи остальных частей модели и витрин
|
||||
появляются вместе с ними.
|
||||
|
||||
Замена партиций не используется для `ods.order`: версии прошлых дней
|
||||
доливаются, а прямое чтение Kafka не задаёт границы полного слепка ([ADR
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in new issue
Block a user