# Задание 3: `dm.session_overview` **Чему учит:** агрегат хранит слагаемые, а не готовые средние и доли. Среднее по строкам витрины не складывается никогда, а уникальных нельзя складывать даже тогда, когда на сегодняшних данных сумма сходится. ## Роль витрины Дневной портрет визитов в разрезе устройств: сколько было сессий, какие они были длинные, сколько страниц смотрели и как часто доходили до покупки. Вопрос витрины — «как люди ходят по магазину с телефона и с настольного компьютера, и чем эти визиты отличаются». Сессия на стенде — расчетная сущность, ее уже нарезал слой DDS. Собирать визиты заново из событий не нужно и нельзя: это была бы вторая нарезка, которая тихо разойдется с первой. Витрина с таким именем была и на прошлом стенде, но зерно у нее сменилось, и сменилось не по вкусу. Там визитов как сущности не было, и витрина нарезала их сама, строкой на визит. Здесь визит — сущность слоя DDS, и витрина, повторяющая его зерно, оказалась бы обычным представлением по [правилу формы](../architecture/dm.md): копией сессий с другими именами колонок. Чтобы задание осталось про дневной агрегат, витрина поднялась на зерно выше — до дня и среза. ## Зерно и форма Зерно — **день сессии × категория устройства**. Источник — `dds.session_v`, у него зерно визита, значит зерно сменилось и витрина материализуется: `dm.session_overview_rep` / `_dist`, донор `_stage_rep` / `_stage_dist`, публичное `dm.session_overview_v`. Ключ партиции — день. Категория устройства взята из сессии как есть: у куки она постоянна, и слой DDS уже перевел код источника в слово. Словарь расшифровки шире, чем мир стенда: слов в нем четыре, а встречаются три — `desktop`, `mobile` и `tablet`. Строку под `tv` заводить незачем, она возникнет сама, если такие визиты появятся. ## Состав колонок | Колонка | Тип | Что это | |---|---|---| | `report_date` | `Date` | день сессии | | `device_category` | `LowCardinality(String)` | категория устройства | | `sessions` | `UInt64` | число визитов | | `visitors` | `UInt64` | число разных `client_id` | | `bounce_sessions` | `UInt64` | визиты ровно из одного события | | `purchase_sessions` | `UInt64` | визиты, в которых была хотя бы одна покупка | | `duration_seconds_total` | `UInt64` | суммарная длительность визитов | | `events_total` | `UInt64` | событий во всех визитах | | `pageviews` | `UInt64` | просмотров страниц | | `cart_adds` | `UInt64` | добавлений в корзину | | `purchases` | `UInt64` | покупок | | `_load_id` | `String` | запуск, собравший день | | `_load_ts` | `DateTime64(3, 'UTC')` | когда собран | Средней длительности и доли отказов в витрине нет намеренно. Хранить `avg_duration_seconds` было бы удобнее ровно один раз — пока смотрят одну строку. Тот, кто захочет среднее по дню целиком, сложит средние по трем устройствам и получит неправду: средние не складываются, они взвешиваются. Пара «`duration_seconds_total` и `sessions`» дает верный ответ на любом срезе, а деление стоит потребителю один вызов. То же и с долей отказов: храним `bounce_sessions` и `sessions`, долю считает тот, кто спрашивает. С `visitors` история тоньше, и в ней главная ловушка задания. Складывать уникальных по строкам нельзя: один и тот же посетитель, попавший в две строки, будет посчитан дважды. Проверьте на стенде — и увидите, что сумма `visitors` по устройствам за день **сходится** с `dm.daily_traffic_v`. Это не отменяет правила, а показывает, откуда берется совпадение: в мире стенда устройство приписано куке на всю жизнь ([словарь понятий](../../CONTEXT.md), «Паспорт куки»), поэтому одна кука лежит ровно в одной строке дня, и пересекаться строкам не в чем. Совпадение это — свойство мира, а не свойство витрины. Появятся визиты одной куки с двух устройств — сумма поедет, и ни одна проверка об этом не скажет: отчет просто начнет тихо завышать аудиторию. Отсюда правило, которое стоит унести с задания: складывать уникальных нельзя даже там, где сегодня получается. Число посетителей дня спрашивают у `dm.daily_traffic_v`, где оно посчитано на своем зерне. ## Ритм пересборки Источник — сессии: день замерзает, как только прожит. Объем прогона тот же, что в задании 2, — хвост от первого несобранного прожитого дня; механика та же: донор и замена дневных партиций. ## Ключ шардирования Ваше решение по [правилу донора](../architecture/storage.md), «Раскладка по шардам». Здесь у измерения всего три встречающихся значения, и распределены они неровно: планшеты — тонкая полоса, а телефоны и настольные машины держат основную массу. Прежде чем брать хеш от одного `device_category`, посчитайте, сколько строк ляжет на каждый шард, и назовите, чем вас устраивает результат. ## Как проверить себя Деловой ключ — `device_category` внутри дня. Соберите прямой агрегат из `dds.session_v` за день, соедините с `dm.session_overview_v` полным внешним соединением и посчитайте строки, где ключа нет с одной из сторон или где расходится любой счетчик. Ноль — зеленый. Две проверки покрупнее ловят грубую ошибку сразу: - сумма `sessions` за день равна числу строк `dds.session_v` этого дня: ни один визит не потерян и ни один не задвоен; - сумма `events_total` за день равна сумме `events_total` по сессиям того же дня. Если суммы событий сходятся, а сумма сессий больше числа визитов — почти наверняка витрина собрана из событий, а не из сессий. ## Приемка - [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется ключом цели; - [ ] `dm.session_overview_v` перечисляет колонки явно; - [ ] источник витрины — `dds.session_v`, своей нарезки визитов в ней нет; - [ ] средних и долей в физической таблице нет; - [ ] задача `session_overview` в `homework_check` зеленая; - [ ] задача витрины добавлена в `dm_transform`.