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

+11 -7
View File
@@ -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.
+7 -3
View File
@@ -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` —
+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` между разными ключами
шардирования.
+18 -12
View File
@@ -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`
и обёртку-представление — а урок версий менти уже отыграл.
+22 -14
View File
@@ -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` колонкой в строке сессии.** Чужой чекан в своей модели: наша
+22 -5
View File
@@ -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
+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).