# Задание 2: `dm.top_pages_daily` **Чему учит:** зерно сменилось — значит витрина материализуется, живет дневными партициями и встает в конвейер. Первая настоящая витрина: пять объектов, донор, замена партиции, задача в `dm_transform`. ## Роль витрины Популярность страниц по дням: сколько раз страницу открыли, сколько разных посетителей ее видели и что на ней делали кроме просмотра. Торговое событие садится на ту страницу, где случилось: корзина — на карточку товара, покупка — на страницу подтверждения заказа. Поэтому у карточки товара рядом стоят просмотры и корзины, и их отношение — конверсия карточки — главный вопрос к этой витрине: какие товары кладут в корзину, едва открыв, а какие смотрят и уходят. Воронки «просмотр → корзина → покупка» здесь нет и быть не может: корзина и покупка живут на разных страницах, и связать их шаги в одну цепочку на зерне страницы не из чего. Путь одного человека собирают из событий или из сессий, а витрина страниц отвечает про страницу. ## Зерно и форма Зерно — **день события × страница**. Источник — события, у него зерно события, значит зерно сменилось и витрина материализуется: `dm.top_pages_daily_rep` / `_dist`, донор `_stage_rep` / `_stage_dist`, публичное `dm.top_pages_daily_v`. Страница — это `page_path`, путь `url` без домена и параметров запроса; считается он здесь же, из колонки `url` источника. Полный адрес в зерно не берется: с параметрами запроса одна и та же страница рассыпалась бы на сотни строк, и витрина перестала бы отвечать на свой вопрос. Ключ партиции — день: он же единица пересборки. Источник — `dds.event_v`, публичный договор события. Витрина задания 1 источником не служит: витрина слоя DM читает договор DDS, как и стендовые витрины, и не зависит от того, выполнена ли соседняя домашка. ## Состав колонок | Колонка | Тип | Что это | |---|---|---| | `report_date` | `Date` | день события | | `page_path` | `LowCardinality(String)` | путь страницы | | `pageviews` | `UInt64` | число событий `pageview` на странице | | `visitors` | `UInt64` | число разных `client_id` на странице | | `cart_adds` | `UInt64` | число событий `add_to_cart` на странице | | `purchases` | `UInt64` | число событий `purchase` на странице | | `_load_id` | `String` | запуск, собравший день | | `_load_ts` | `DateTime64(3, 'UTC')` | когда собран | Счетчики условные: строку дают все три типа событий, а различает их `countIf`. Отфильтровать источник по `event_type = 'pageview'`, как подсказывает имя витрины, — значит получить витрину из одних просмотров: страницы останутся на месте, а `cart_adds` и `purchases` у всех строк обнулятся. Ошибка тихая: витрина выглядит собранной, и главный ее вопрос — конверсия карточки — молча отвечает «ноль везде». ## Ритм пересборки Источник — события: день замерзает сразу, как только прожит. Значит пересобирать нужно только то, что еще не собрано: хвост дней, которых в витрине нет, — от первого несобранного прожитого дня до последнего прожитого. Готовые дни трогать незачем, а вот дыру, оставленную частичным отказом, объем прогона обязан подобрать: ищи не последний собранный день, а первый несобранный. Механика — сборка отрезка в донор одной вставкой и замена дневных партиций по одной, как у `revenue_daily`. ## Ключ шардирования Твое решение по [правилу донора](../architecture/storage.md), «Раскладка по шардам». Подумай, чем эта витрина отличается от `revenue_daily`: там кроме дня было одно измерение из шести близких по объему значений, здесь — страницы, которых много и которые по популярности очень неровные. ## Как проверить себя Деловой ключ — `page_path` внутри дня. Собери прямой агрегат из `dds.event_v` за день, соедини с `dm.top_pages_daily_v` полным внешним соединением по ключу и посчитай строки, где ключа нет с одной из сторон или где расходится любой из четырех счетчиков. Ноль — зеленый. Две проверки попроще, ими удобно ловить грубую ошибку до полного соединения: - сумма `pageviews` за день равна числу событий `pageview` этого дня в `dds.event_v`; - сумма `purchases` за день равна числу событий `purchase` этого дня. Проверки ловят разное. Фильтр `WHERE event_type = 'pageview'` первая из них пропускает: отбор строк по типу не меняет счет событий того же типа. Расхождение покажет только вторая — вместе с `cart_adds` обнулятся и `purchases`. Проверку выбирают по тому, умеет ли она покраснеть на твоей ошибке, а не по тому, как быстро она пишется: [проверка, которая не умеет краснеть, бесполезна](../architecture/testing.md). `visitors` складывать по строкам нельзя: один посетитель ходит по разным страницам и в сумме посчитается несколько раз. Число посетителей дня спрашивают у `dm.daily_traffic_v`, а не у этой витрины. ## Приемка - [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется ключом цели; - [ ] `dm.top_pages_daily_v` перечисляет колонки явно, включая `_load_id` и `_load_ts`; - [ ] объем прогона выводится из состояния витрины, а не задается руками; - [ ] задача `top_pages_daily` в `homework_check` зеленая; - [ ] задача витрины добавлена в `dm_transform`, повторный прогон конвейера результат не меняет.