Merge pull request 'Слой витрин DM: форма по зерну, дележ с менти, контур DQ' (#147) from 135-dm-marts into main

Reviewed-on: #147
This commit was merged in pull request #147.
This commit is contained in:
ddmitry committed 2026-08-25 13:38:21 +03:00
commit f1fee5f1d1
3 files changed
+164 -9

No files matched your search

-4
View File
@@ -54,10 +54,6 @@
- Наполняет слой [дневной конвейер](../etl.md): запуск, объём прогона и
замена партиций на кластере — его устройство.
## Открытые решения
- Витрины DM — #135.
## Родословная
Дом набора и первая сущность собраны развилкой #130 карты #127; форма
+155
View File
@@ -0,0 +1,155 @@
# Слой 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));
- счётчики классов расхождений против описи мира — по дням с закрытым
окном;
- счётные сверки витрин стенда с их 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.
+9 -5
View File
@@ -14,8 +14,8 @@
Дневной конвейер на стенде один — даг `etl_pipeline`. Своей работы
у него нет: он дёргает слойные даги последовательно и ждёт каждый —
`dds_transform`, затем `dm_transform`. Контур DQ этапа 4 встанет его же
звеном после DM; состав звена — предмет развилки витрин (#135) и нарезки.
`dds_transform`, затем `dm_transform`. Контур DQ этапа 4 встаёт его же
звеном после DM; состав звена — в [доке слоя DM](dm.md).
Слойные даги устроены по образцу `orders_ingest`: без расписания, один
активный прогон, запускаются триггером конвейера или рукой при разборе
@@ -42,7 +42,8 @@
Параметров у конвейера нет: объём он выводит сам из `world_position` и
состояния слоёв. Сессии догоняют хвост — от последнего собранного дня до
последнего прожитого; заказы пересобирают дышащее окно K от позиции; карта
идентичностей пересчитывается целиком; правила витрин — у витрин (#135).
идентичностей пересчитывается целиком; правила витрин — у
[слоя DM](dm.md).
Поэтому один и тот же запуск одинаково верен из триггера, с кнопки менти и
после падения, а разгону на семь дней хватает одного прогона.
@@ -62,7 +63,9 @@
вставка через Distributed раскладывает строки по шардам, — затем
`REPLACE PARTITION` на кластере атомарно подменяет кусок каждого шарда. Эта
механика — часть самих трансформаций и строится этапом 4: без неё дневной
прогон не повторяем.
прогон не повторяем. Сущности, пересчитываемые целиком, идемпотентны и без
замены: полная перезапись в `ReplacingMergeTree` — так живут карта
идентичностей и витрина `daily_traffic` ([слой DM](dm.md)).
## Не входит
@@ -70,7 +73,8 @@
диапазон, включая форму необязательных параметров того же дага, — этап 5
(#7); туда же стык #111 про дату модельного мира и `logical_date`.
- Инкрементальный ETL вместо пересборки — техдолг #2.
- Состав витрин `dm_transform` и звена DQ — #135 и нарезка этапа 4.
- Состав витрин `dm_transform` и звена DQ — [дока слоя DM](dm.md);
имена задач — нарезка этапа 4.
## Отклонённые варианты