feat(dm): добавлены витрины выручки и трафика

Зачем:
- Показать два ритма пересборки агрегатов на готовых витринах DM.

Что:
- Добавлены физические модели и публичные договоры revenue_daily и daily_traffic.
- Добавлен dm_transform с оконной заменой выручки и полным пересчетом трафика.
- Отрицательный опыт локального соединения через публичные _v зафиксирован; выбран явный GLOBAL LEFT JOIN.
- Замены партиций сериализованы после подтвержденного OOM при трех одновременных задачах.
- Обновлены архитектурные документы и мастер-спека.

Проверка:
- make lint, make config-test, make smoke, make check-clickhouse, make check-services.
- Чистая сборка world_recreate, dds_transform и dm_transform; меры сверены с прямыми агрегатами.
This commit is contained in:
ddadmin committed 2026-08-26 13:55:26 -04:00
1 parent c9bcacd63b
commit da0477f0f1
12 files changed
+436 -25

No files matched your search

+2 -4
View File
@@ -89,10 +89,8 @@
- Сборка сессий: ключи нарезки (`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` (пара «посетители / известные» через
`dds.identity_map_v`).
- Витрины `dm.events_enriched_v`, `dm.top_pages_daily_v` и
`dm.utm_effectiveness_v` (`declared_revenue` берется из `purchase_revenue`).
- Событийная сторона сверки `dm.purchase_vs_orders_v`: `purchase_id`,
`purchase_revenue`, `event_date`.
- DQ-сверка сессий с эталоном читает не его, а `ods.event_v` —
+54 -8
View File
@@ -31,8 +31,8 @@
витрины, на него ссылаются учебные задания;
- `purchase_vs_orders` — сверка A+C с классами расхождений; зерно — заказ,
шардирование сохраняет ко-локацию заказов по `cityHash64(order_id)`;
- `daily_traffic` — пара «посетители» / «известные пользователи», локальное
обогащение [картой идентичностей](dds/identity.md);
- `daily_traffic` — пара «посетители» / «известные пользователи», обогащение
[картой идентичностей](dds/identity.md);
- `dq_summary` — сводка проверок качества (ниже).
Остальные витрины спеки — учебные задания: менти строит их сам по ТЗ с
@@ -55,8 +55,8 @@
- **от событий и сессий** — день замерзает сразу; пересборка догоняет
хвост несобранных дней заменой дневных партиций;
- **от заказов** — день дышит окном K и закрывается; пересборка заменяет
партиции окна, дни за ним закрыты по построению;
- **от заказов** — день дышит, пока входит в последний слепок; пересборка
заменяет партиции этого отрезка, дни за ним закрыты по построению;
- **от карты идентичностей** — не замерзает никогда: склейка
ретроспективна, покупка сегодня делает куку известной во всех её прошлых
днях, а карта пересчитывается целиком.
@@ -64,12 +64,58 @@
Поэтому `daily_traffic` наследует у своего источника и ритм, и механику:
полный пересчёт всех дней каждый прогон в `ReplacingMergeTree(_load_ts)`,
как у самой карты. Дневных партиций у неё нет: партиция — единица замены,
а эта витрина заменяется только целиком. Пересчёт дешёв:
витрина читает сессии и карту локальным соединением по ключу ко-локации.
а эта витрина заменяется только целиком. На стенде такая работа остается
быстрой; витрина читает сессии и карту через их публичные договоры.
Пересчитывать точечно только затронутые дни не из чего: для этого надо
знать, какие связки новые, а полный пересчёт карты этот след затирает —
восстановление следа оставлено лабе повышенной сложности (issue #149).
## Витрины стенда
Оба публичных представления перечисляют деловые колонки и координаты запуска
`_load_id`, `_load_ts` явно. Новая колонка физической таблицы сама в договор не
попадет.
### `dm.revenue_daily_v`
Зерно: день заказа × категория товара. Витрина читает только оплаченные
заказы из `dds.order_v`, синхронно разворачивает три массива позиций и узнает
категорию артикула через `dic.products`. Один заказ с несколькими позициями
категории считается в `orders` один раз. `units` складывает количество единиц,
`revenue` складывает цену позиции × количество. Скидка, доставка и итог заказа
между категориями не делятся. `aov` равен `revenue / orders` в том же срезе и
хранит два знака после запятой.
Первый запуск строит всю доступную историю. Следующие берут границы дней из
последнего слепка ODS: только источник знает, какие даты еще может изменить.
Пропущенный оплаченный день расширяет левую границу, чтобы частичный отказ
не оставил дыру после следующего прогона. Агрегат при этом читает DDS; ODS
отвечает только на вопрос об объеме.
`dm.revenue_daily_rep` нарезана по `report_date`; день собирается в
таблице-доноре и заменяется через `REPLACE PARTITION`. Цель и донор шардируются
по `cityHash64(product_category)`. Кроме дня, у витрины только категория, а
шесть категорий стартового мира близки по объему. Более сложный составной ключ
распределил бы эту маленькую таблицу без нового урока.
### `dm.daily_traffic_v`
В строке один день. `visitors` считает разные `client_id` из
`dds.session_v`; `known_users` считает разные `user_id` после левого соединения
с полной текущей `dds.identity_map_v`. Анонимная сессия остается в витрине.
Несколько кук одного человека увеличивают `visitors`, но дают одного
`known_user`, поэтому `known_users <= visitors`.
Сессии и карта лежат по `client_id`, но публичные `_v` скрывают
`Distributed`-таблицы от локальной подстановки. На ClickHouse 26.3
`EXPLAIN PLAN` показал чтение обоих шардов на инициаторе: настройка
`distributed_product_mode = 'local'` локального соединения не дала. Поэтому
витрина использует явный `GLOBAL LEFT JOIN` через публичные договоры, а не
подменяет их физическими таблицами. Это дороже по сети, зато граница слоев
остается явной. Новая связь куки меняет прошлые дни, поэтому каждый запуск
пишет полную историю новой версией в `ReplacingMergeTree(_load_ts)`, а
публичное представление выбирает текущие строки через `FINAL`.
## `dm.dq_summary`
Зерно — «день данных × проверка»: строка отвечает, каково качество дня
@@ -113,7 +159,7 @@ Airflow. Партиция — день данных; прогон звена DQ
## Остаётся нарезке
- Имена задач `dm_transform`, звена DQ и дага самопроверки.
- Имена звена DQ и дага самопроверки.
- Ключи шардирования остальных материализованных витрин — вместе с их
DDL; правило партиции-донора — в [доке хранилища](storage.md).
- Тексты учебных заданий и точный список проверок обоих контуров.
@@ -136,7 +182,7 @@ Airflow. Партиция — день данных; прогон звена DQ
расшифровывать. Красное «ещё не построено» честнее.
- **Самопроверка пишет в `dq_summary`.** Попытки менти смешались бы с
историей качества конвейера.
- **Окно K для `daily_traffic` — прошлое замерзает.** Дни за окном хранили
- **Окно заказов для `daily_traffic` — прошлое замерзает.** Дни за окном хранили
бы число известных на момент закрытия — витрина тихо расходится с картой
и протаскивает обратно «знание на дату», отвергнутое развилкой карты
(#133).
+11 -5
View File
@@ -521,8 +521,10 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
| `20-ods-tables.sql` | типизированное событие, версии заказа и обе таблицы ошибок |
| `30-ods-views.sql` | актуальные события, текущие заказы и матвью разбора в ODS |
| `40-stg-views.sql` | матвью приёма: чтец в сырьё |
| `45-dds-tables.sql` | физические модели заказа и сессии, доноры их партиций |
| `50-dds-views.sql` | публичные договоры события, заказа и сессии на языке модели DDS |
| `45-dds-tables.sql` | физические модели заказа, сессии и карты идентичностей, доноры партиций |
| `50-dds-views.sql` | публичные договоры сущностей DDS |
| `55-dm-tables.sql` | физические витрины выручки и трафика, донор партиций выручки |
| `60-dm-views.sql` | публичные договоры витрин DM |
Справочники идут сразу за базами: зона `dic` не зависит ни от одного слоя, и
правила порядка слоёв к ней не применяются.
@@ -589,6 +591,11 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
| `dds` | `dds.session_v` | публичный договор сессии |
| `dds` | `dds.identity_map_rep` / `_dist` | известная пара куки с пользователем; таблицы полного пересчёта |
| `dds` | `dds.identity_map_v` | точный публичный договор карты идентичностей с `FINAL` |
| `dm` | `dm.revenue_daily_rep` / `_dist` | дневная выручка по категориям |
| `dm` | `dm.revenue_daily_stage_rep` / `_dist` | таблица-двойник, донор дневных партиций выручки |
| `dm` | `dm.revenue_daily_v` | публичный договор дневной выручки |
| `dm` | `dm.daily_traffic_rep` / `_dist` | версии дневных счетчиков трафика |
| `dm` | `dm.daily_traffic_v` | точные текущие счетчики трафика с `FINAL` |
| `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором |
| `dic` | `dic.products` | словарь товаров поверх подложки |
@@ -598,9 +605,8 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
В слое DDS построены событие, заказ, сессия и карта идентичностей. Событие
остаётся представлением над ODS; заказ и сессия гасят повтор заменой партиции;
карта пишет полный набор пар в версионную таблицу. Слой DM принесут следующие
тикеты этапа. Его состав задан разделом 7 мастер-спеки и переносится сюда по
мере постройки.
карта пишет полный набор пар в версионную таблицу. В DM выручка заменяет
партиции изменяемых дней, а трафик пишет полную историю новой версией.
## Что проверено
+7 -8
View File
@@ -367,8 +367,10 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
ошибка в `*_errors`, а не молчаливые нули: имена CamelCase регистрозависимы,
опечатка иначе не падает.
- **Политика соединений**: по ключу ко-локации — обычное соединение с
комментарием, почему локальный результат корректен; по любому другому ключу
— только явный GLOBAL; `NOT IN` — только `GLOBAL NOT IN`. Сверка
комментарием, почему локальный результат корректен; если публичные
представления скрывают ко-локацию — решение записывается рядом с витриной.
По любому другому ключу — только явный GLOBAL; `NOT IN` — только
`GLOBAL NOT IN`. Сверка
`purchase`↔заказ —
легитимная GLOBAL-витрина (заказы малы).
- **Конвейер без TRUNCATE**: поток версий — в ReplacingMergeTree, точное чтение
@@ -505,8 +507,9 @@ Kafka день переигрывается генератором заново:
счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется
`declared_revenue` по UTM.
- **`daily_traffic_v`** — расширяется парой «посетители» / «известные
пользователи» (обогащение через `dds.identity_map_v`, локальное соединение
по ключу ко-локации).
пользователи». Обогащение идет явным `GLOBAL LEFT JOIN` через
`dds.identity_map_v`: опыт на ClickHouse 26.3 показал, что публичные `_v`
скрывают ко-локацию от локальной подстановки.
- `events_enriched_v`, `top_pages_daily_v`, `session_overview_v`,
`dq_errors_daily_v` — переезжают на новую модель без смены роли: источник —
`dds.event_v`, не `ods.event`.
@@ -684,10 +687,6 @@ Kafka Engine на двух нодах снят с этого списка при
`ReplacingMergeTree`. Проверять то, что заведомо случается и заведомо
обезврежено, — работа без ответа на конце.
- Точную форму локального обогащения `daily_traffic_v` через публичные `_v` —
эмпирически при проектировании витрин #135. Общее поведение соединения двух
Distributed-таблиц уже проверено в
[исследовании](../research/2026-08-22-distributed-join.md).
- Размер артефакта эталонного мира после пересборки.
- Спорные API (Airflow Datasets/сенсоры, ClickHouse DDL) — перед кодом
сверять через MCP Context7 (правило AGENTS.md).