Files
clickstream-data-platform/docs/architecture/dm.md
T
ddadmin 5f317ba8d2 docs(warehouse): упрощены объяснения витрин, сессий и пересчета
Зачем: читателю ступеней 1–4 нужно проследить выбор дней, расчет строки
и публикацию результата без расшифровки метафор.

Что: согласованы справочники DM, сессий, ETL и два раздела хранилища
с комментариями шести SQL-примеров. Уточнены ограничения повторного
запуска и выбора атрибутов при равном времени событий.

Проверка: make lint, git diff --check, локальные Markdown-ссылки и якоря.
SQL без комментариев совпадает; AST Python без строк документации совпадает.
Исторические разделы и материалы вне границ задачи сохранены.

Refs: #249
2026-09-13 14:33:39 -04:00

283 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Слой DM: витрины
Витрины готовят данные DDS для отчетов и дашбордов. Здесь описаны форма
хранения, правила обновления и проверки. Состав колонок задан в
[спецификации](../specs/2026-07-30-stand-v2-realism.md#витрины-dm)
и [учебных заданиях](../labs/README.md).
## Таблица или представление
Зерно — то, чему соответствует одна строка. Например, в заказах это заказ,
а в дневной выручке — день и категория товара. На стенде результат
с новым зерном хранится в таблице: отчет читает готовые итоги без
повторного расчета. Таблицу наполняет `dm_transform`.
У такой витрины есть локальная `_rep`, распределенная `_dist` и публичное
представление `_v` с явным списком колонок. Для замены дневных партиций
служат еще две таблицы: `_stage_rep` и `_stage_dist`. Это донор — место
сборки результата перед публикацией. Подробности — в
[справочнике хранилища](storage.md#имена-объектов).
Если запрос только переименовывает или дополняет поля и сохраняет одну
строку на событие, достаточно обычного представления. Оно выполняет
запрос при чтении, не хранит вторую копию событий и не требует пересборки.
На небольших данных стенда и агрегаты могли бы вычисляться при чтении
([исследование](../research/2026-08-22-view-predicate-pushdown.md)).
Готовые таблицы нужны в программе для практики загрузки, обновления
и включения витрин в конвейер.
## Что готово в стенде, а что остаётся заданием
Готовы три витрины дашборда и сводка качества:
- `revenue_daily` — одна строка на день заказа и категорию товара;
образец дневной пересборки для учебных заданий.
- `purchase_vs_orders` — одна строка на заказ: сопоставление покупки
из кликстрима с заказом магазина и класс расхождения.
- `daily_traffic` — одна строка на день: число посетителей и известных
пользователей с учетом [карты идентичностей](dds/identity.md).
- `dq_summary` — одна строка на день данных и проверку качества.
В [заданиях ступени 1](../labs/README.md) вы строите `events_enriched_v`,
`top_pages_daily`, `session_overview`, `dq_errors_daily` и `utm_effectiveness`.
Для каждой физической витрины нужно добавить сборку в `dm_transform`.
Представление `events_enriched_v` отдельной задачи не требует.
Стендовый дашборд читает готовые витрины и работает до выполнения заданий.
Дашборд по своим витринам можно собрать в
[факультативе](../labs/optional/superset-dashboard.md) после ступени 2.
Устройство стендового дашборда — в [справочнике Superset](superset.md).
## Ритм пересборки
Дни для пересчета выбирают по тому, как меняется источник:
- События и сессии прожитого дня на стенде больше не меняются. Витрины
по ним собирают диапазон от первого пропущенного дня до последнего
прожитого и заменяют дневные партиции.
- Заказы могут меняться в течение семи модельных дней. Последний слепок ODS
задает диапазон дат для пересборки; пропущенные дни могут расширить его.
- Новые связи карты идентичностей меняют трактовку прошлых визитов:
сегодняшняя покупка позволяет узнать пользователя и в ранних днях.
Поэтому `daily_traffic`, как и карта, пересчитывается целиком.
`daily_traffic` каждый раз добавляет всю историю новой версией
в `ReplacingMergeTree(_load_ts)`. Дневных партиций у нее нет, а текущие
строки выбирает представление через `FINAL`.
У `purchase_vs_orders` два источника. Левая граница — самая ранняя
из трех дат: начало окна последнего слепка, первый пропущенный день
и первый день с `awaiting_order`. Последний нужен потому, что строка
ожидания уже есть в витрине и поиском пропусков не обнаружится.
Правая граница — более поздняя из конца окна заказов и последнего
прожитого дня с покупкой. Один запуск пересобирает весь отрезок,
даже если с предыдущего запуска прошло больше семи модельных дней.
Для учебной сводки брака действует отдельное правило: дни выбираются
по времени загрузки, а прежние итоги сохраняются после удаления
источника по TTL. Оно разобрано в
[задании `dq_errors_daily`](../labs/1-4-dq-errors-daily.md).
## Витрины стенда
Публичные представления перечисляют деловые колонки и координаты запуска
`_load_id`, `_load_ts` явно. Новая колонка физической таблицы сама в договор не
попадет.
### `dm.revenue_daily_v`
Одна строка — день заказа и категория товара. Витрина читает только оплаченные
заказы из `dds.order_v`, синхронно разворачивает три массива позиций и узнает
категорию артикула через `dic.products`. Один заказ с несколькими позициями
категории считается в `orders` один раз. `units` складывает количество единиц,
`revenue` складывает цену позиции × количество. Скидка и доставка
в выручку не входят; итог заказа между категориями не делится. `aov` равен `revenue / orders` в том же срезе и
хранит два знака после запятой.
Первый запуск строит всю доступную историю. Следующие берут границы дней из
последнего слепка ODS: только источник знает, какие даты еще может изменить.
Если в DDS есть оплаченные заказы за день, отсутствующий в витрине,
левая граница расширяется до него. Агрегат читает DDS; ODS отвечает
только на вопрос об объеме.
В группе `revenue_daily` дага `dm_transform` три шага:
1. [`revenue_daily_scope.sql`](../../sql/dm/revenue_daily_scope.sql)
возвращает `first_day` и `last_day`. Без заказов в ODS или DDS
запрос не возвращает строк, и задача завершается ошибкой.
2. [`revenue_daily_rebuild.sql`](../../sql/dm/revenue_daily_rebuild.sql)
очищает донор и одной вставкой собирает весь диапазон в
`dm.revenue_daily_stage_dist`. Вставка ждет доставки на шарды.
3. [`revenue_daily_replace.sql`](../../sql/dm/revenue_daily_replace.sql)
заменяет в `dm.revenue_daily_rep` каждую дневную партицию `report_date`
готовой партицией `dm.revenue_daily_stage_rep`.
Повтор замены не добавляет копий строк. Ограничения при частичном отказе
описаны в [конвейере](etl.md#идемпотентность).
Цель и донор шардируются
по `cityHash64(product_category)`. Кроме дня, у витрины только категория, а
шесть категорий стартового мира близки по объему. Более сложный составной ключ
распределил бы эту маленькую таблицу без нового урока.
### `dm.purchase_vs_orders_v`
Одна строка — один заказ. Покупки из `dds.event_v` сначала сворачиваются по
`purchase_id[1]`: контракт источника обещает один номер и одну объявленную
сумму. Отдельный счет событий позволяет обнаружить повтор покупки
и присвоить класс `duplicate_event`. Заказы приходят из `dds.order_v`. Обе стороны записаны
подзапросами, затем соединены `FULL OUTER GLOBAL JOIN`: левая
Distributed-таблица без подзапроса вернула бы каждый непарный заказ
по разу с каждого шарда
([исследование](../research/2026-08-22-distributed-join.md)).
Непарная сторона остается `NULL` благодаря `join_use_nulls = 1`. При
`join_use_nulls = 0` ClickHouse заполнил бы ее нулем или пустой строкой и скрыл
отсутствие источника. Клиентская `declared_revenue` и серверная `items_total`
сравниваются как `Decimal(18, 2)`. Классы проверяются по порядку:
`awaiting_order` для покупки без заказа, затем `cancelled`, `lost_event`,
`duplicate_event`, `amount_delta`, `match`. Поэтому отмененный заказ без
события остается отмененным, а не потерянным.
Партиции витрины разделены по `order_day`, цель и донор шардируются по
`cityHash64(order_id)`. Диапазон собирается в доноре одной вставкой и
заменяется по дням. Первичная сборка охватывает всю историю обоих источников;
следующие прогоны выбирают дни по правилам обоих источников, описанным выше.
### `dm.daily_traffic_v`
В строке один день. `visitors` считает разные `client_id` из
`dds.session_v`; `known_users` считает разные `user_id` после левого соединения
с полной текущей `dds.identity_map_v`. Анонимная сессия остается в витрине.
Несколько кук одного человека увеличивают `visitors`, но дают одного
известного пользователя, поэтому `known_users <= visitors`.
Сессии и карта распределены по `client_id`. Однако на ClickHouse 26.3
чтение через публичные `_v` с `distributed_product_mode = 'local'`
не дало локального соединения. Поэтому в запросе явно указан
`GLOBAL LEFT JOIN`. Он сохраняет сессии без известного пользователя;
в `known_users` учитываются только ненулевые `user_id`.
Обновление всей истории новой версией описано в разделе
[«Ритм пересборки»](#ритм-пересборки).
## `dm.dq_summary`
Одна строка `dm.dq_summary_v` — день данных и одна проверка.
Строка отвечает, каково качество дня сейчас; историю запусков хранит Airflow.
Партиция совпадает с днём данных и заменяется через `REPLACE PARTITION`.
Деловые колонки сводки:
- `data_date` — проверяемый день;
- `check_name` — имя проверяемого правила;
- `reference_rows` — число деловых ключей на эталонной стороне;
- `actual_rows` — число деловых ключей в проверяемом объекте;
- `failed_rows` — число отсутствующих или различающихся ключей;
- `status` — `pass` при нуле нарушений, иначе `fail`.
Служебные `_load_id` и `_load_ts` называют собравший запуск. Представление
перечисляет колонки явно. Порогов и промежуточного состояния нет.
## Звено DQ конвейера
Даг `dq_check` стоит в `etl_pipeline` после `dm_transform` и проверяет пять
правил:
- `sessions_vs_reference`: ключ — `client_id` внутри дня, эталон —
`uniqExact(VisitID)` из `ods.event_v`, объект — число строк
`dds.session_v`;
- `classes_vs_inventory`: ключ — `mismatch_class`, эталон — счётчик из
`orders` в паспорте мира, объект — число заказов класса в
`dm.purchase_vs_orders_v`. Проверяются `match`, `cancelled`, `lost_event`,
`duplicate_event` и `amount_delta` только для покрытых паспортом закрытых
дней;
- `revenue_daily_vs_dds`: ключ — `product_category` внутри дня. Сравниваются
`orders`, `units`, `revenue` и `aov` с прямым агрегатом `dds.order_v`;
- `purchase_vs_orders_vs_dds`: ключ — `order_id` внутри дня. Все деловые
поля сверки сравниваются с независимой сборкой из `dds.event_v` и
`dds.order_v`;
- `daily_traffic_vs_dds`: ключ — день. `visitors` и `known_users`
сравниваются с прямыми агрегатами `dds.session_v` и
`dds.identity_map_v`.
Область каждой проверки выводится из объединения сторон. Полностью потерянный
день или ключ поэтому не исчезает из результата. Разные ошибки тоже не могут
скомпенсировать друг друга общим итогом: `failed_rows` считается после
сопоставления деловых ключей.
`classes_vs_inventory` сравнивает результат с паспортом мира. Это техническая
проверка доставки и обработки эталонных данных стенда. Она не проверяет
сам генератор; у настоящего магазина такого эталона нет.
Четыре SQL-сверки считают дневные итоги в ClickHouse и возвращают Airflow только
сводку и малый срез диагностики. Техническая сверка с паспортом читает небольшой
файл JSON в Python. Все пять сборок пишут в общий донор. После их успеха даг
заменяет проверенные дневные партиции. Затем именованные задачи читают
опубликованную сводку и падают при `failed_rows > 0`. Расхождение успевает
сохраниться до красного завершения, а ошибка сборки не затирает прежний
результат. Красная задача печатает не больше двадцати нарушенных ключей за
проверку со значениями сторон. Для полного списка итоговую агрегацию SQL-сверки
заменяют на `SELECT * FROM comparison WHERE failed ORDER BY data_date,
business_key`.
Если после починки день исчез с обеих сторон, его больше нет и в новом доноре.
Такой день удаляется из сводки отдельно: старая красная строка не должна
превращаться в историю попыток.
## Даг самопроверки
`homework_check` запускается вручную и содержит задачу для каждого из
[пяти заданий по витринам](../labs/README.md). Если витрины нет или значения
расходятся с источником, задача завершается ошибкой. Журнал показывает
причину и примеры несовпадений. Успех означает, что проверяемые значения
совпали; остальные требования задания вы проверяете сами.
Ожидаемые значения рассчитываются по исходным данным текущего стенда.
Для четырех заданий это события или сессии DDS, для сводки брака —
таблицы ошибок ODS. Сравнение по деловому ключу учитывает отсутствующие
строки с обеих сторон. Результаты не пишутся в `dq_summary`:
самопроверка учебных витрин отделена от качества готового конвейера.
Готовые SQL-проверки открывайте после собственного решения: они содержат
расчет существенной части задания.
## Отклонённые варианты
- **Весь слой представлениями.** Дёшево и всегда свежо, но главный
производственный паттерн слоя — пересборка агрегатов — остался бы
невидим, а `dm_transform` — без работы.
- **Материализовать всё.** Копия событий ради переименования колонок —
лишняя; такой пример приучал бы без нужды дублировать исходные данные.
- **Гибрид по цене запроса.** Выбор формы по стоимости, которой на объёмах
стенда нет: сторож без настоящей опасности.
- **Проверки домашек в конвейерном звене DQ.** Конвейер стоял бы красным до
выполнения лаб, а проверка, которая всегда красная, приучает себя
игнорировать.
- **«Жёлтый» статус недостроенной витрины.** У задачи Airflow нет такого
состояния; изображать его логами — семантика, которую пришлось бы
расшифровывать. Красное «ещё не построено» честнее.
- **Самопроверка пишет в `dq_summary`.** Результаты учебных попыток смешались бы
с историей качества конвейера.
- **Окно заказов для `daily_traffic` — прошлое замерзает.** Дни за окном хранили
бы число известных на момент закрытия — витрина тихо расходится с картой
и протаскивает обратно «знание на дату», отвергнутое развилкой карты
(#133).
- **Инкрементальный пересчёт затронутых дней по `first_matched_at`.**
Производственный ход, но на объёмах стенда его экономия неизмерима, а
след новых связок требует вернуть колонку, выкинутую в #133. Стал лабой
повышенной сложности — issue #149.
- **Superset поверх DDS без слоя DM.** Терялся бы урок «витрина — контракт
под потребителя», против раздела 7 спеки.
## Связанные решения
- [Мастер-спека](../specs/2026-07-30-stand-v2-realism.md), разделы 4 и 7, —
состав колонок витрин и классы сверки.
- [Дневной конвейер](etl.md) — запуск `dm_transform`, объём прогона,
замена партиций на кластере.
- [Дока хранилища](storage.md) — имена объектов, конвенция чтения через `_v`,
партиция-донор.
- [Карта модели DDS](dds/README.md) — источники витрин.
- Резолюция — тикет #135 карты #127.