Зачем: - Показать слой DDS как договор данных, которому не нужна физическая копия. - Сохранить простой учебный стенд без отдельного маршрута ради узких прав. Что: - Добавлено кластерное представление dds.event_v с явными 44 колонками. - Роли bi_reader разрешено чтение ODS, DDS и DM при договоре чтения через _v. - Согласованы мастер-спека, ADR, справочники архитектуры и живые тикеты этапа. - Зафиксировано исследование поведения DEFINER поверх Distributed. Проверка: - make config-test - make smoke - make check-clickhouse - Приёмочные запросы: равенство ODS/DDS, состав колонок, коды устройств и права bi.
160 lines
13 KiB
Markdown
160 lines
13 KiB
Markdown
# Слой DM: витрины
|
||
|
||
Как модель DDS превращается в витрины для потребителя BI: какую форму
|
||
принимает витрина, кто какие витрины строит и как слой проверяется. Состав
|
||
колонок каждой витрины задаёт раздел 7 мастер-спеки; здесь — форма,
|
||
наполнение и контроль.
|
||
|
||
## Форма: по зерну, не по цене запроса
|
||
|
||
Витрина, меняющая зерно источника — дневной агрегат, сверка по заказам, —
|
||
материализуется: `dm.<имя>_rep` / `_dist` и публичное `dm.<имя>_v` с явным
|
||
списком колонок, по общим [конвенциям хранилища](storage.md). Наполняет её
|
||
`dm_transform`; ритм и механика пересборки — ниже. Витрина, сохраняющая зерно
|
||
источника — переименование, обогащение строки, — остаётся обычным
|
||
представлением: копию событий ради других имён колонок не держит ни один
|
||
боевой склад.
|
||
|
||
Правило выбрано учебной целью, а не ценой запроса. На объёмах стенда
|
||
представлениями обошлось бы всё: условия сквозь цепочку представлений
|
||
проходят ([исследование](../research/2026-08-22-view-predicate-pushdown.md)).
|
||
Но пересборка агрегатов конвейером — производственный паттерн, ради
|
||
которого стенд и строится, а пара «таблица рядом с представлением»
|
||
показывает: форма — решение по роли витрины, у одного слоя законны обе.
|
||
|
||
## Кто строит: стенд и менти
|
||
|
||
Стенд привозит готовыми витрины трёх сюжетов дашборда этапа 8 и
|
||
инфраструктуру контроля:
|
||
|
||
- `revenue_daily` — выручка по дням и категориям; образец материализованной
|
||
витрины, на него ссылаются учебные задания;
|
||
- `purchase_vs_orders` — сверка A+C с классами расхождений; зерно — заказ,
|
||
шардирование сохраняет ко-локацию заказов по `cityHash64(order_id)`;
|
||
- `daily_traffic` — пара «посетители» / «известные пользователи», локальное
|
||
обогащение [картой идентичностей](dds/identity.md);
|
||
- `dq_summary` — сводка проверок качества (ниже).
|
||
|
||
Остальные витрины спеки — учебные задания: менти строит их сам по ТЗ с
|
||
образцом. Разминка — представление `events_enriched_v` (зерно события),
|
||
затем агрегаты `top_pages_daily`, `session_overview` и `dq_errors_daily`,
|
||
последней — самая содержательная `utm_effectiveness` с уроком `declared_*`.
|
||
Построенная витрина встаёт в конвейер руками менти: финальный пункт
|
||
каждого ТЗ — добавить задачу своей витрины в `dm_transform`. До этого
|
||
конвейер о ней не знает и остаётся зелёным, после — витрина пересобирается
|
||
наравне со стендовыми.
|
||
|
||
Дашборд этапа 8 стоит только на витринах стенда: приёмка этапа не зависит
|
||
от выполненных домашек. Расширение дашборда визуализациями по своим
|
||
витринам — отдельная домашка после этапа 8.
|
||
|
||
## Ритм пересборки
|
||
|
||
День витрины пересобирается, пока может измениться его источник, —
|
||
поэтому ритмов три, по числу источников:
|
||
|
||
- **от событий и сессий** — день замерзает сразу; пересборка догоняет
|
||
хвост несобранных дней заменой дневных партиций;
|
||
- **от заказов** — день дышит окном K и закрывается; пересборка заменяет
|
||
партиции окна, дни за ним закрыты по построению;
|
||
- **от карты идентичностей** — не замерзает никогда: склейка
|
||
ретроспективна, покупка сегодня делает куку известной во всех её прошлых
|
||
днях, а карта пересчитывается целиком.
|
||
|
||
Поэтому `daily_traffic` наследует у своего источника и ритм, и механику:
|
||
полный пересчёт всех дней каждый прогон в `ReplacingMergeTree(_load_ts)`,
|
||
как у самой карты. Дневных партиций у неё нет: партиция — единица замены,
|
||
а эта витрина заменяется только целиком. Пересчёт дешёв:
|
||
витрина читает сессии и карту локальным соединением по ключу ко-локации.
|
||
Пересчитывать точечно только затронутые дни не из чего: для этого надо
|
||
знать, какие связки новые, а полный пересчёт карты этот след затирает —
|
||
восстановление следа оставлено лабе повышенной сложности (issue #149).
|
||
|
||
## `dm.dq_summary`
|
||
|
||
Зерно — «день данных × проверка»: строка отвечает, каково качество дня
|
||
сейчас, а не сколько раз его проверяли, — история прогонов и так живёт в
|
||
Airflow. Партиция — день данных; прогон звена DQ заменяет партиции
|
||
проверенных дней через `REPLACE PARTITION`. Обещанный спекой перевод с
|
||
`TRUNCATE+INSERT` выполняется той же механикой замены, что и у витрин, —
|
||
ничего отдельного учить не надо.
|
||
|
||
## Звено DQ конвейера
|
||
|
||
Встаёт в `etl_pipeline` после DM ([конвейер](etl.md)) и проверяет только
|
||
объекты стенда:
|
||
|
||
- счётная сверка сессий с эталоном `VisitID` — кука × день, эталон читается
|
||
из `ods.event_v` ([модель сессии](dds/session.md));
|
||
- счётчики классов расхождений против описи мира — по дням с закрытым
|
||
окном. Проверка техническая, не урок DQ: её оракул — опись генератора,
|
||
а у настоящего магазина оракула нет. Обещание узкое и честное: до
|
||
витрин доехал тот мир, который родился, — одна растяжка через весь
|
||
путь вместо сторожей на каждом шаге. Верно ли сам мир рождён, она
|
||
не видит: опись считает тот же генератор;
|
||
- счётные сверки витрин стенда с их DDS-источниками.
|
||
|
||
Результаты ложатся в `dm.dq_summary`. Звено — не только сторож конвейера,
|
||
но и экспонат: менти видит, как контроль качества устроен в бою — по
|
||
расписанию, с историей, рядом с трансформациями.
|
||
|
||
## Даг самопроверки
|
||
|
||
Домашки контролирует отдельный даг без расписания: менти запускает его
|
||
рукой. Задача витрины красная, пока витрины нет или счёт не сходится, и
|
||
зелёная, когда построено верно; сообщение об ошибке говорит, что не так и
|
||
куда смотреть. Проверки — инварианты против DDS-источников, золотые
|
||
константы не прибиваются. В `dq_summary` даг не пишет: он инструмент лабы,
|
||
а не история качества конвейера.
|
||
|
||
Приём проверен на соседнем учебном стенде Airflow + Greenplum:
|
||
валидационный даг с группами проверок по слоям давал менти обратную связь
|
||
по каждому шагу домашки, не трогая рабочий конвейер.
|
||
|
||
## Остаётся нарезке
|
||
|
||
- Имена задач `dm_transform`, звена DQ и дага самопроверки.
|
||
- Ключи шардирования остальных материализованных витрин — вместе с их
|
||
DDL; правило партиции-донора — в [доке хранилища](storage.md).
|
||
- Тексты учебных заданий и точный список проверок обоих контуров.
|
||
- Механика сверки A+C в SQL — рамка в спеке, разделы 4 и 7.
|
||
|
||
## Отклонённые варианты
|
||
|
||
- **Весь слой представлениями.** Дёшево и всегда свежо, но главный
|
||
производственный паттерн слоя — пересборка агрегатов — остался бы
|
||
невидим, а `dm_transform` — без работы.
|
||
- **Материализовать всё.** Копия событий ради переименования колонок —
|
||
антипаттерн, который менти увёз бы в бой.
|
||
- **Гибрид по цене запроса.** Выбор формы по стоимости, которой на объёмах
|
||
стенда нет: сторож без настоящей опасности.
|
||
- **Проверки домашек в конвейерном звене DQ.** Конвейер стоял бы красным до
|
||
выполнения лаб, а проверка, которая всегда красная, приучает себя
|
||
игнорировать.
|
||
- **«Жёлтый» статус недостроенной витрины.** У задачи Airflow нет такого
|
||
состояния; изображать его логами — семантика, которую пришлось бы
|
||
расшифровывать. Красное «ещё не построено» честнее.
|
||
- **Самопроверка пишет в `dq_summary`.** Попытки менти смешались бы с
|
||
историей качества конвейера.
|
||
- **Окно K для `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.
|