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`) — физическая сущность: одна строка — заказ в
|
- **Заказ** (`dds.order_v`, хранение `dds.order_rep` / `_dist`) — физическая
|
||||||
текущем состоянии, позиции массивами в строке, наполняется заменой
|
сущность: одна строка — заказ в текущем состоянии, позиции массивами в
|
||||||
партиций — [модель заказа](order.md).
|
строке, наполняется заменой партиций — [модель заказа](order.md).
|
||||||
- **Сессия** (`dds.session`) — расчётная сущность: одна строка — визит,
|
- **Сессия** (`dds.session_v`, хранение `dds.session_rep` / `_dist`) —
|
||||||
|
расчётная сущность: одна строка — визит,
|
||||||
нарезку по правилу визита стенд делает сам, `VisitID` генератора — эталон
|
нарезку по правилу визита стенд делает сам, `VisitID` генератора — эталон
|
||||||
сверки; наполнение заменой дневной партиции — [модель сессии](session.md).
|
сверки; наполнение заменой дневной партиции — [модель сессии](session.md).
|
||||||
- **Событие** (`dds.event_v`) — контракт над ODS, а не копия: представление,
|
- **Событие** (`dds.event_v`) — контракт над ODS, а не копия: представление,
|
||||||
переводящее событие на язык бизнес-модели, — [контракт события](event.md).
|
переводящее событие на язык бизнес-модели, — [контракт события](event.md).
|
||||||
- **Карта идентичностей** (`dds.identity_map`) — кука ↔ пользователь
|
- **Карта идентичностей** (`dds.identity_map_v`, хранение
|
||||||
магазина; сборку решает развилка #133.
|
`dds.identity_map_rep` / `_dist`) — кука ↔ пользователь магазина —
|
||||||
|
[модель карты](identity.md).
|
||||||
|
|
||||||
Связи держатся двумя ключами моста (спека, разделы 4 и 5): `purchaseID[1]`
|
Связи держатся двумя ключами моста (спека, разделы 4 и 5): `purchaseID[1]`
|
||||||
события равен `order_id` заказа — по нему идут сверка и сборка склейки;
|
события равен `order_id` заказа — по нему идут сверка и сборка склейки;
|
||||||
@@ -43,13 +45,15 @@
|
|||||||
- DDS обслуживает человека с дашбордом: время лежит в поясе счётчика,
|
- DDS обслуживает человека с дашбордом: время лежит в поясе счётчика,
|
||||||
пересчёт — один раз при наполнении слоя ([дока хранилища](../storage.md),
|
пересчёт — один раз при наполнении слоя ([дока хранилища](../storage.md),
|
||||||
«Часовые пояса»).
|
«Часовые пояса»).
|
||||||
|
- Публичный договор каждой сущности — `_v`; роли объектов и проверяемая
|
||||||
|
граница доступа заданы [конвенциями хранилища](../storage.md), «Имена
|
||||||
|
объектов».
|
||||||
- Служебные колонки — координаты собственной загрузки слоя; чужие ярлыки не
|
- Служебные колонки — координаты собственной загрузки слоя; чужие ярлыки не
|
||||||
переносятся, а спрашиваются у их слоя по ключу сущности. Вывод правила и
|
переносятся, а спрашиваются у их слоя по ключу сущности. Вывод правила и
|
||||||
цепочка расследования — [модель заказа](order.md).
|
цепочка расследования — [модель заказа](order.md).
|
||||||
|
|
||||||
## Открытые решения
|
## Открытые решения
|
||||||
|
|
||||||
- Сборка `dds.identity_map` — развилка #133 карты #127.
|
|
||||||
- Запуск трансформаций и механика замены партиций на кластере — #134 и
|
- Запуск трансформаций и механика замены партиций на кластере — #134 и
|
||||||
этап 5.
|
этап 5.
|
||||||
- Витрины DM — #135.
|
- Витрины DM — #135.
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
индекс перестал бы её анализировать. Цена `FINAL` — отключённый
|
индекс перестал бы её анализировать. Цена `FINAL` — отключённый
|
||||||
автоматический `PREWHERE` — свойство ODS, представление её не усугубляет.
|
автоматический `PREWHERE` — свойство ODS, представление её не усугубляет.
|
||||||
|
|
||||||
|
Как публичный договор DDS, представление перечисляет колонки явно и следует
|
||||||
|
общей [границе доступа](../storage.md), «Имена объектов».
|
||||||
|
|
||||||
## Язык модели
|
## Язык модели
|
||||||
|
|
||||||
Имена атрибутов — имена модели, а не нормализованные имена источника
|
Имена атрибутов — имена модели, а не нормализованные имена источника
|
||||||
@@ -38,7 +41,7 @@
|
|||||||
выбор линзы слой сделал за читателя, UTC остаётся языком ODS.
|
выбор линзы слой сделал за читателя, UTC остаётся языком ODS.
|
||||||
|
|
||||||
`ClientID` остаётся `client_id`: это устоявшееся имя куки во всей
|
`ClientID` остаётся `client_id`: это устоявшееся имя куки во всей
|
||||||
экосистеме Метрики, по нему шардирование и мост к `dds.identity_map`;
|
экосистеме Метрики, по нему шардирование и мост к `dds.identity_map_v`;
|
||||||
своё имя здесь создало бы третий синоним. `client_timezone` тоже остаётся —
|
своё имя здесь создало бы третий синоним. `client_timezone` тоже остаётся —
|
||||||
это атрибут посетителя, а не линза времени.
|
это атрибут посетителя, а не линза времени.
|
||||||
|
|
||||||
@@ -79,12 +82,13 @@ issue #143.
|
|||||||
|
|
||||||
## Читатели
|
## Читатели
|
||||||
|
|
||||||
- Сборка `dds.session`: ключи нарезки (`client_id`, `event_time`,
|
- Сборка сессий: ключи нарезки (`client_id`, `event_time`,
|
||||||
`event_date`), тип события, страница, UTM, паспорт куки — категория
|
`event_date`), тип события, страница, UTM, паспорт куки — категория
|
||||||
устройства приходит уже расшифрованной.
|
устройства приходит уже расшифрованной.
|
||||||
- Витрины `dm.events_enriched_v`, `dm.top_pages_daily_v`,
|
- Витрины `dm.events_enriched_v`, `dm.top_pages_daily_v`,
|
||||||
`dm.utm_effectiveness_v` (`declared_revenue` — из `purchase_revenue`),
|
`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`,
|
- Событийная сторона сверки `dm.purchase_vs_orders_v`: `purchase_id`,
|
||||||
`purchase_revenue`, `event_date`.
|
`purchase_revenue`, `event_date`.
|
||||||
- DQ-сверка сессий с эталоном читает не его, а `ods.event_v` —
|
- 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` (день заказа
|
`dds.order_rep` — `ReplicatedMergeTree` без колонки версии: партиция —
|
||||||
неизменен), ключ сортировки — `order_id`, шардирование распределённой
|
`order_date` (день заказа неизменен), ключ сортировки — `order_id`.
|
||||||
таблицы — `cityHash64(order_id)`, тем же выражением, что у ODS: это
|
`dds.order_dist` шардирует по `cityHash64(order_id)`, тем же выражением, что
|
||||||
единственный устойчивый ключ сущности.
|
ODS: это единственный устойчивый ключ сущности.
|
||||||
|
|
||||||
Идемпотентность держит замена партиции, а не версии. Сбор дня — батч после
|
Идемпотентность держит замена партиции, а не версии. Сбор дня — батч после
|
||||||
прихода слепка: трансформация собирает партицию заново из `ods.order_v` и
|
прихода слепка: трансформация собирает партицию заново из `ods.order_v` и
|
||||||
атомарно подменяет. Поэтому каждая строка таблицы — ровно один заказ, и
|
атомарно подменяет. Поэтому каждая строка физической таблицы — ровно один
|
||||||
таблица сама служит поверхностью чтения слоя: голый `SELECT`, без `FINAL` и
|
заказ, а чтению не нужен `FINAL`. Пока окно K живо, день «дышит» — статусы
|
||||||
без обёртки-представления. Пока окно K живо, день «дышит» — статусы заказов
|
заказов меняются, и партиция дня пересобирается очередным запуском; на выходе
|
||||||
меняются, и партиция дня пересобирается очередным запуском; на выходе из
|
из окна день замерзает.
|
||||||
окна день замерзает.
|
|
||||||
|
Публичная поверхность — `dds.order_v`: обычное представление с явным списком
|
||||||
|
деловых и собственных служебных колонок над `dds.order_dist`, без `FINAL`.
|
||||||
|
Роли объектов и доступ следуют общей [конвенции](../storage.md), «Имена
|
||||||
|
объектов».
|
||||||
|
|
||||||
Механика запуска — где собирается партиция и как подменяется на кластере —
|
Механика запуска — где собирается партиция и как подменяется на кластере —
|
||||||
сюда не входит: это устройство трансформаций (#134, этап 5).
|
сюда не входит: это устройство трансформаций (#134, этап 5).
|
||||||
@@ -86,7 +90,8 @@ JSON происходит один раз, на границе ODS → DDS, ку
|
|||||||
- Сверка `dm.purchase_vs_orders_v`: `order_id`, `order_date`, `items_total`,
|
- Сверка `dm.purchase_vs_orders_v`: `order_id`, `order_date`, `items_total`,
|
||||||
`status`; соединение — GLOBAL JOIN с обеими сторонами-подзапросами
|
`status`; соединение — GLOBAL JOIN с обеими сторонами-подзапросами
|
||||||
([исследование соединений](../../research/2026-08-22-distributed-join.md)).
|
([исследование соединений](../../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.event_v`, а разбор
|
||||||
позиций и пересчёт времён при каждом чтении собирали бы модель заново на
|
позиций и пересчёт времён при каждом чтении собирали бы модель заново на
|
||||||
лету.
|
лету. Публичное `dds.order_v` поверх готовой физической модели этому доводу
|
||||||
|
не противоречит: оно ничего заново не вычисляет.
|
||||||
- **`ReplacingMergeTree(updated_at)` ещё раз.** Повторил бы механику ODS
|
- **`ReplacingMergeTree(updated_at)` ещё раз.** Повторил бы механику ODS
|
||||||
вторым разом: физически хранил бы устаревшие состояния, вернул бы `FINAL`
|
вторым разом: физически хранил бы устаревшие состояния, вернул бы `FINAL`
|
||||||
и обёртку-представление — а урок версий менти уже отыграл.
|
и обёртку-представление — а урок версий менти уже отыграл.
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
свою нарезку с эталоном. Стенд здесь играет роль сессионизатора: в бою её
|
свою нарезку с эталоном. Стенд здесь играет роль сессионизатора: в бою её
|
||||||
играет Метрика, которая режет визиты у себя и с октября 2025 отдаёт готовый
|
играет Метрика, которая режет визиты у себя и с октября 2025 отдаёт готовый
|
||||||
`VisitID` прямо в хитах. Поэтому стенд считает сессии сам, а `VisitID`
|
`VisitID` прямо в хитах. Поэтому стенд считает сессии сам, а `VisitID`
|
||||||
генератора использует только как эталон самопроверки — иначе сборка
|
генератора использует только как эталон самопроверки — иначе сборка сессий
|
||||||
`dds.session` выродилась бы в `GROUP BY` по чужому полю и центральный урок
|
выродилась бы в `GROUP BY` по чужому полю и центральный урок
|
||||||
слоя пропал бы. Без этого абзаца действия стенда выглядят странно: «зачем
|
слоя пропал бы. Без этого абзаца действия стенда выглядят странно: «зачем
|
||||||
считать то, что уже дано колонкой».
|
считать то, что уже дано колонкой».
|
||||||
|
|
||||||
@@ -35,10 +35,10 @@
|
|||||||
ломают.
|
ломают.
|
||||||
|
|
||||||
`session_id` — не ключ сортировки и не ключ шардирования, а колонка-ручка:
|
`session_id` — не ключ сортировки и не ключ шардирования, а колонка-ручка:
|
||||||
витрины сессий читают таблицу напрямую, их зерно совпадает с зерном модели,
|
витрины сессий читают `dds.session_v`, их зерно совпадает с зерном модели, и в
|
||||||
и в соединениях id не участвует — события его не несут, мост к заказам живёт
|
соединениях id не участвует — события его не несут, мост к заказам живёт на
|
||||||
на событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со
|
событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со стендовой и
|
||||||
стендовой и назвать расхождение одним числом) и разбор DQ-расхождений.
|
назвать расхождение одним числом) и разбор DQ-расхождений.
|
||||||
|
|
||||||
## Состав строки
|
## Состав строки
|
||||||
|
|
||||||
@@ -64,10 +64,11 @@
|
|||||||
|
|
||||||
## Движок и наполнение
|
## Движок и наполнение
|
||||||
|
|
||||||
`MergeTree` без колонки версии, по образцу заказа: партиция — день сессии,
|
`dds.session_rep` — `ReplicatedMergeTree` без колонки версии, по образцу
|
||||||
ключ сортировки — кука и начало сессии, шардирование распределённой таблицы —
|
заказа: партиция — день сессии, ключ сортировки — кука и начало сессии.
|
||||||
`cityHash64(ClientID)`, тем же выражением, что у событий, — ко-локация с
|
`dds.session_dist` шардирует по `cityHash64(client_id)`, тем же выражением,
|
||||||
`dds.identity_map` ([дока хранилища](../storage.md), «Раскладка по шардам»).
|
что события, — это ко-локация с `dds.identity_map_rep` ([дока
|
||||||
|
хранилища](../storage.md), «Раскладка по шардам»).
|
||||||
Совпадение ключа с событиями не случайно: все события куки лежат на одном
|
Совпадение ключа с событиями не случайно: все события куки лежат на одном
|
||||||
шарде, поэтому сессионизация локальна по природе — капкан смены ключа при
|
шарде, поэтому сессионизация локальна по природе — капкан смены ключа при
|
||||||
вставке, который дока хранилища обещает сборкам DDS, у сессий не стреляет.
|
вставке, который дока хранилища обещает сборкам DDS, у сессий не стреляет.
|
||||||
@@ -78,10 +79,15 @@
|
|||||||
целиком; идемпотентность держит замена партиции. В отличие от заказов, где
|
целиком; идемпотентность держит замена партиции. В отличие от заказов, где
|
||||||
батч пересобирает до K «дышащих» дней, у сессий пересобирается только одна
|
батч пересобирает до K «дышащих» дней, у сессий пересобирается только одна
|
||||||
партиция: день самодостаточен и замерзает сразу (правило спеки, раздел 6).
|
партиция: день самодостаточен и замерзает сразу (правило спеки, раздел 6).
|
||||||
Внутри живого дня `dds.session` про сегодня честно молчит — «сегодня»
|
Внутри живого дня модель сессии про сегодня честно молчит — «сегодня»
|
||||||
смотрят через `dds.event_v`; сессия и по смыслу не финальна, пока день не
|
смотрят через `dds.event_v`; сессия и по смыслу не финальна, пока день не
|
||||||
прожит.
|
прожит.
|
||||||
|
|
||||||
|
Публичная поверхность — `dds.session_v`: обычное представление с явным
|
||||||
|
списком деловых и собственных служебных колонок над `dds.session_dist`.
|
||||||
|
Роли объектов и доступ следуют общей [конвенции](../storage.md), «Имена
|
||||||
|
объектов».
|
||||||
|
|
||||||
Источник сборки — `dds.event_v` ([контракт события](event.md)): сессии
|
Источник сборки — `dds.event_v` ([контракт события](event.md)): сессии
|
||||||
нужны от него ключи нарезки, тип события, страница, UTM и паспорт куки.
|
нужны от него ключи нарезки, тип события, страница, UTM и паспорт куки.
|
||||||
Механика запуска — где собирается партиция и как подменяется на кластере —
|
Механика запуска — где собирается партиция и как подменяется на кластере —
|
||||||
@@ -102,7 +108,7 @@
|
|||||||
## Читатели
|
## Читатели
|
||||||
|
|
||||||
- Витрина обзора сессий `session_overview_v`: длительности, глубина,
|
- Витрина обзора сессий `session_overview_v`: длительности, глубина,
|
||||||
вход/выход; читает таблицу напрямую, зерно совпадает (#135).
|
вход/выход; читает `dds.session_v`, зерно совпадает (#135).
|
||||||
- DQ-сверка с эталоном (см. выше).
|
- DQ-сверка с эталоном (см. выше).
|
||||||
- Лаба сессий: менти собирает нарезку сам до знакомства с кодом стенда,
|
- Лаба сессий: менти собирает нарезку сам до знакомства с кодом стенда,
|
||||||
потом сравнивает — с эталоном и со стендовой.
|
потом сравнивает — с эталоном и со стендовой.
|
||||||
@@ -124,9 +130,11 @@
|
|||||||
- **Гибрид: сборка по `VisitID`, DQ пересчитывает нарезку.** Сторож на
|
- **Гибрид: сборка по `VisitID`, DQ пересчитывает нарезку.** Сторож на
|
||||||
нарезку, которой сборка не делает; та же работа, что в варианте «считать
|
нарезку, которой сборка не делает; та же работа, что в варианте «считать
|
||||||
сам», но урок спрятан в проверку.
|
сам», но урок спрятан в проверку.
|
||||||
- **Представление вместо таблицы.** Оконная сессионизация на каждое чтение;
|
- **Одно динамическое представление вместо физической таблицы.** Оконная
|
||||||
|
сессионизация на каждое чтение;
|
||||||
урок «слой — контракт, а не копия» уже закреплён за `dds.event_v` — доводы
|
урок «слой — контракт, а не копия» уже закреплён за `dds.event_v` — доводы
|
||||||
модели заказа, здесь они только сильнее.
|
модели заказа, здесь они только сильнее. Публичное `dds.session_v` поверх
|
||||||
|
готовой физической модели ничего заново не считает.
|
||||||
- **Счётчик или UUID вместо детерминированного хеша.** Пересборка партиции
|
- **Счётчик или UUID вместо детерминированного хеша.** Пересборка партиции
|
||||||
выдала бы другие id — замена перестала бы быть идемпотентной.
|
выдала бы другие id — замена перестала бы быть идемпотентной.
|
||||||
- **`VisitID` колонкой в строке сессии.** Чужой чекан в своей модели: наша
|
- **`VisitID` колонкой в строке сессии.** Чужой чекан в своей модели: наша
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ CSV-файла. Живёт он вне цепочки слоёв, в зоне `d
|
|||||||
| `_kafka` | таблица на движке `Kafka` |
|
| `_kafka` | таблица на движке `Kafka` |
|
||||||
| `_file` | таблица на движке `File` — чтец файла |
|
| `_file` | таблица на движке `File` — чтец файла |
|
||||||
| `_mv` | материализованное представление |
|
| `_mv` | материализованное представление |
|
||||||
| `_v` | обычное представление |
|
| `_v` | обычное представление, публичная поверхность чтения |
|
||||||
|
|
||||||
Суффикс носит каждый физический объект. Голого имени у таблицы не существует:
|
Суффикс носит каждый физический объект. Голого имени у таблицы не существует:
|
||||||
запрос к `stg.hits_raw` даёт громкую ошибку «нет такой таблицы» — а под голым
|
запрос к `stg.hits_raw` даёт громкую ошибку «нет такой таблицы» — а под голым
|
||||||
@@ -63,6 +63,23 @@ CSV-файла. Живёт он вне цепочки слоёв, в зоне `d
|
|||||||
`order`, `session`. Граница между «как привезли» и «что это такое»
|
`order`, `session`. Граница между «как привезли» и «что это такое»
|
||||||
проходит по STG, и имена её показывают.
|
проходит по 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`. Локальные таблицы остаются для чтения и обслуживания —
|
Пишем только в `_dist`. Локальные таблицы остаются для чтения и обслуживания —
|
||||||
@@ -98,10 +115,10 @@ FROM источник_dist`. Заводское значение `parallel_distr
|
|||||||
|
|
||||||
Ключи ко-локации названы заранее, потому что на них стоит политика соединений из
|
Ключи ко-локации названы заранее, потому что на них стоит политика соединений из
|
||||||
раздела 6 спеки: обычное соединение разрешено только по ключу ко-локации, всё
|
раздела 6 спеки: обычное соединение разрешено только по ключу ко-локации, всё
|
||||||
прочее — через `GLOBAL`. Значит `dds.session` и `dds.identity_map` шардируются по
|
прочее — через `GLOBAL`. Значит `dds.session_dist` и `dds.identity_map_dist`
|
||||||
`cityHash64(ClientID)`. Объекты DDS с зерном заказа должны сохранять ко-локацию
|
шардируются по `cityHash64(client_id)`. Объекты DDS с зерном заказа сохраняют
|
||||||
по `cityHash64(order_id)`; ключи остальных частей будущей модели и витрин
|
ко-локацию по `cityHash64(order_id)`; ключи остальных частей модели и витрин
|
||||||
появятся вместе с ними.
|
появляются вместе с ними.
|
||||||
|
|
||||||
Замена партиций не используется для `ods.order`: версии прошлых дней
|
Замена партиций не используется для `ods.order`: версии прошлых дней
|
||||||
доливаются, а прямое чтение Kafka не задаёт границы полного слепка ([ADR
|
доливаются, а прямое чтение Kafka не задаёт границы полного слепка ([ADR
|
||||||
|
|||||||
@@ -224,14 +224,15 @@ Ecommerce (заполнены только у торговых событий):
|
|||||||
неизменного `created_at`, шардирование по `cityHash64(order_id)`.
|
неизменного `created_at`, шардирование по `cityHash64(order_id)`.
|
||||||
`snapshot_date` остаётся датой наблюдения источника, но не задаёт публикацию
|
`snapshot_date` остаётся датой наблюдения источника, но не задаёт публикацию
|
||||||
или идемпотентность. Физическое чтение может видеть несколько версий;
|
или идемпотентность. Физическое чтение может видеть несколько версий;
|
||||||
`ods.order_v` возвращает точное текущее состояние. Устройство модели DDS и
|
`ods.order_v` возвращает точное текущее состояние. В DDS одна строка хранит
|
||||||
способ её материализации решаются отдельно.
|
один заказ с позициями в типизированных массивах; физическая модель и
|
||||||
|
пакетное наполнение описаны в [доке заказа](../architecture/dds/order.md).
|
||||||
|
|
||||||
Пропущенный день ничего не ломает, следующий слепок самовосстанавливает.
|
Пропущенный день ничего не ломает, следующий слепок самовосстанавливает.
|
||||||
- `items` остаётся сырой строкой JSON в ODS. Разбирать позиции следует на
|
- `items` остаётся сырой строкой JSON в ODS. Позиции разбираются на границе
|
||||||
границе ODS → DDS, но их представление определяется вместе с будущей моделью
|
ODS → DDS в типизированные массивы строки заказа. Это остаётся носителем
|
||||||
заказов. Это остаётся носителем навыка «вложенный JSON в ClickHouse», не
|
навыка «вложенный JSON в ClickHouse», не превращая приём в преждевременную
|
||||||
превращая приём в преждевременную модель данных.
|
модель данных.
|
||||||
- Статусы держим все три: смена `created` → `paid` и есть причина «дыхания»
|
- Статусы держим все три: смена `created` → `paid` и есть причина «дыхания»
|
||||||
выручки внутри окна; сужение до двух — резервный срез 1. На выходе из окна
|
выручки внутри окна; сужение до двух — резервный срез 1. На выходе из окна
|
||||||
заказ либо `paid`, либо `cancelled`: неоплаченного отменяют, «навсегда
|
заказ либо `paid`, либо `cancelled`: неоплаченного отменяют, «навсегда
|
||||||
@@ -292,12 +293,19 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
|
|||||||
- **Карта соответствий кука↔пользователь** строится трансформацией из уже
|
- **Карта соответствий кука↔пользователь** строится трансформацией из уже
|
||||||
существующего моста: `purchase`-событие (`ClientID`, `purchaseID`) ↔ заказ
|
существующего моста: `purchase`-событие (`ClientID`, `purchaseID`) ↔ заказ
|
||||||
(`order_id`, `user_id`). Ни новых полей, ни нового транспорта.
|
(`order_id`, `user_id`). Ни новых полей, ни нового транспорта.
|
||||||
- Форма и место карты: таблица `dds.identity_map`
|
- Форма и место карты: `dds.identity_map_rep` хранит пару `client_id` UInt64,
|
||||||
(`client_id` UInt64, `user_id` UInt64, `first_matched_at` DateTime),
|
`user_id` UInt64 и координаты собственной загрузки `_load_id`, `_load_ts` в
|
||||||
ReplacingMergeTree, `ORDER BY (client_id, user_id)`, шардирование по
|
`ReplicatedReplacingMergeTree(_load_ts)` с `ORDER BY (client_id, user_id)`.
|
||||||
`cityHash64(client_id)` — тем же выражением, что события (иначе ко-локации
|
`dds.identity_map_dist` шардирует по `cityHash64(client_id)` — тем же
|
||||||
нет); ко-локация делает обогащение витрин локальным;
|
выражением, что события; ко-локация позволяет обогащать витрины локально.
|
||||||
заказы при сборке карты подтягиваются через GLOBAL JOIN.
|
Публичный интерфейс `dds.identity_map_v` перечисляет колонки явно и прячет
|
||||||
|
`FINAL`. `first_matched_at` в стенде нет: у него нет читателя; история
|
||||||
|
склейки оставлена самостоятельной работе в issue #145.
|
||||||
|
- Карта пересобирается пакетно после модели заказов: все `purchase` из
|
||||||
|
`dds.event_v` соединяются с текущими заказами `dds.order_v` через
|
||||||
|
`GLOBAL JOIN`, статус заказа связь покупателя с кукой не отменяет. Каждый
|
||||||
|
запуск пишет полный текущий набор различных пар; предыдущий результат не
|
||||||
|
служит источником следующего.
|
||||||
- **N:1**: часть покупателей покупает с двух кук («телефон и ноутбук») —
|
- **N:1**: часть покупателей покупает с двух кук («телефон и ноутбук») —
|
||||||
параметр мира; значение фиксирует эта спека: 15%, детерминировано
|
параметр мира; значение фиксирует эта спека: 15%, детерминировано
|
||||||
от seed. Ядро лабы:
|
от seed. Ядро лабы:
|
||||||
@@ -422,16 +430,20 @@ README.
|
|||||||
| STG | `stg.orders_raw_kafka`, `stg.orders_raw`, без MV | сырые строки слепка; чтец на ноде 1, забирает пакетный шаг |
|
| STG | `stg.orders_raw_kafka`, `stg.orders_raw`, без MV | сырые строки слепка; чтец на ноде 1, забирает пакетный шаг |
|
||||||
| ODS | `ods.event` (+`_errors`), `ods.event_v` | типизированные версии событий, брак и точное актуальное состояние |
|
| ODS | `ods.event` (+`_errors`), `ods.event_v` | типизированные версии событий, брак и точное актуальное состояние |
|
||||||
| ODS | `ods.order` (+`_errors`), `ods.order_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.event_v` | представление над `ods.event`: snake_case-имена, расшифровка кодов `DeviceCategory`; витрины DM читают его, а не ODS напрямую |
|
||||||
| DDS | модель заказов | зерно, связи и материализация проектируются на этапе DDS |
|
| DDS | `dds.order_rep` / `_dist`, `dds.order_v` | текущая модель заказа и её публичный интерфейс |
|
||||||
| DDS | `dds.identity_map` | карта кука↔пользователь |
|
| DDS | `dds.identity_map_rep` / `_dist`, `dds.identity_map_v` | физическая карта кука↔пользователь и её точный публичный интерфейс |
|
||||||
| DIC | подложка `products_file` и словарь `products` | каталог из CSV, вне цепочки слоёв ([ADR 0012](../adr/0012-dictionary-home.md)) |
|
| DIC | подложка `products_file` и словарь `products` | каталог из CSV, вне цепочки слоёв ([ADR 0012](../adr/0012-dictionary-home.md)) |
|
||||||
| DM | витрины `dm.*_v`, `dm.dq_summary` | см. ниже |
|
| DM | витрины `dm.*_v`, `dm.dq_summary` | см. ниже |
|
||||||
|
|
||||||
У каждой таблицы слоя — пара из локальной и распределённой, имена по конвенции
|
У каждой таблицы слоя — пара из локальной и распределённой, имена по конвенции
|
||||||
суффиксов; она же задаёт служебные колонки, нарезку и срок хранения сырья —
|
суффиксов; она же задаёт служебные колонки, нарезку и срок хранения сырья —
|
||||||
см. [доку хранилища](../architecture/storage.md).
|
см. [доку хранилища](../architecture/storage.md).
|
||||||
|
Потребительские роли и витрины читают сущности DDS через `_v` с явным списком
|
||||||
|
колонок; прав на `_rep` и `_dist` у них нет. Физические объекты остаются ролям
|
||||||
|
загрузки, диагностики и обслуживания. Механизм и проверяемая граница доступа —
|
||||||
|
в [доке хранилища](../architecture/storage.md), «Имена объектов».
|
||||||
|
|
||||||
Заказы принимаются **пакетным забором** ([ADR
|
Заказы принимаются **пакетным забором** ([ADR
|
||||||
0008](../adr/0008-order-ingestion.md), [ADR
|
0008](../adr/0008-order-ingestion.md), [ADR
|
||||||
@@ -494,7 +506,7 @@ Kafka день переигрывается генератором заново:
|
|||||||
счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется
|
счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется
|
||||||
`declared_revenue` по UTM.
|
`declared_revenue` по UTM.
|
||||||
- **`daily_traffic_v`** — расширяется парой «посетители» / «известные
|
- **`daily_traffic_v`** — расширяется парой «посетители» / «известные
|
||||||
пользователи» (обогащение через `dds.identity_map`, локальное соединение
|
пользователи» (обогащение через `dds.identity_map_v`, локальное соединение
|
||||||
по ключу ко-локации).
|
по ключу ко-локации).
|
||||||
- `events_enriched_v`, `top_pages_daily_v`, `session_overview_v`,
|
- `events_enriched_v`, `top_pages_daily_v`, `session_overview_v`,
|
||||||
`dq_errors_daily_v` — переезжают на новую модель без смены роли: источник —
|
`dq_errors_daily_v` — переезжают на новую модель без смены роли: источник —
|
||||||
@@ -670,8 +682,10 @@ Kafka Engine на двух нодах снят с этого списка при
|
|||||||
`ReplacingMergeTree`. Проверять то, что заведомо случается и заведомо
|
`ReplacingMergeTree`. Проверять то, что заведомо случается и заведомо
|
||||||
обезврежено, — работа без ответа на конце.
|
обезврежено, — работа без ответа на конце.
|
||||||
|
|
||||||
- Поведение соединения двух Distributed-таблиц и `distributed_product_mode` —
|
- Точную форму локального обогащения `daily_traffic_v` через публичные `_v` —
|
||||||
эмпирически на стенде (хвост #14).
|
эмпирически при проектировании витрин #135. Общее поведение соединения двух
|
||||||
|
Distributed-таблиц уже проверено в
|
||||||
|
[исследовании](../research/2026-08-22-distributed-join.md).
|
||||||
- Размер артефакта эталонного мира после пересборки.
|
- Размер артефакта эталонного мира после пересборки.
|
||||||
- Спорные API (Airflow Datasets/сенсоры, ClickHouse DDL) — перед кодом
|
- Спорные API (Airflow Datasets/сенсоры, ClickHouse DDL) — перед кодом
|
||||||
сверять через MCP Context7 (правило AGENTS.md).
|
сверять через MCP Context7 (правило AGENTS.md).
|
||||||
|
|||||||
Reference in new issue
Block a user