diff --git a/docs/labs/README.md b/docs/labs/README.md index 0618e4a..d6abd5b 100644 --- a/docs/labs/README.md +++ b/docs/labs/README.md @@ -10,9 +10,9 @@ |---|---|---| | 1 | Пять витрин слоя DM | построить результат для потребителя хранилища; уроки каждой витрины — в таблице ниже | | 2 | [Свой график в Superset](superset-chart.md) | превратить свой результат из хранилища в отчет в BI | -| 3 | Нарежь сессии сам | счет показывает проблему, но причину приходится искать в отдельных строках | +| 3 | Нарежь сессии сам | сравнить свою нарезку визитов с эталоном по числу сессий и найти причину расхождения в отдельных событиях | | 4 | Перепиши нарезку стенда | изменить расчетную сущность внутри готового конвейера и проверить ее внешним эталоном | -| 5 | Сначала сторож, потом ломай: заказы | построить сторожей до рискованного изменения, а не после первой поломки | +| 5 | Сначала сторож, потом ломай: заказы | написать проверки качества до рискованного изменения, а не после первой поломки | | 6 | Разбери сырые события и сверь результат со стендом | изучить разбор потока отдельно от живого приема и сверить его со стендовым результатом | | 7 | Мониторинг: куда смотреть | выбрать, куда и на что смотреть в живой системе | @@ -27,77 +27,73 @@ | № | Результат | Чему учит | |---|---|---| -| 1 | [`events_enriched_v`](events-enriched.md) | слой — это контракт, а не копия данных | -| 2 | [`top_pages_daily`](top-pages-daily.md) | зерно сменилось — витрина материализуется и встает в конвейер | -| 3 | [`session_overview`](session-overview.md) | зерно решает, на какие вопросы витрина уже не ответит | -| 4 | [`dq_errors_daily`](dq-errors-daily.md) | у брака свой день и свой срок жизни | -| 5 | [`utm_effectiveness`](utm-effectiveness.md) | `declared_` в имени — предупреждение, а не украшение | +| 1 | [`events_enriched_v`](events-enriched.md) | строить слой как договор о данных, а не как их копию | +| 2 | [`top_pages_daily`](top-pages-daily.md) | создавать материализованную витрину с дневной пересборкой в конвейере | +| 3 | [`session_overview`](session-overview.md) | хранить слагаемые для расчета средних и долей | +| 4 | [`dq_errors_daily`](dq-errors-daily.md) | считать брак по дню загрузки и хранить итоги дольше источника | +| 5 | [`utm_effectiveness`](utm-effectiveness.md) | читать `declared_` как предупреждение о сумме со слов клиента | Общее устройство слоя — [дока DM](../architecture/dm.md). Она отвечает на вопросы «почему так», задание отвечает на вопрос «что сделать». -## Общие правила ступени 1 +## Как принято на ступени 1 -Они одни для пяти заданий по витринам ступени 1, поэтому записаны здесь, а не -повторяются в каждом ТЗ. +**Читать — через `_v`.** Физические `_rep` и `_dist` служат загрузке и +обслуживанию, а публичный договор DDS — `dds.event_v` или `dds.session_v` — +остается прежним даже при изменении физических объектов. Чтение DDS +напрямую делает задания независимыми: соседняя витрина может быть еще не +построена. +Исключение — задание 4: у таблиц брака публичного договора нет. -**Читать — через `_v`.** Публичный договор сущности DDS — представление с -суффиксом `_v`: `dds.event_v`, `dds.session_v`. Витрина называет только -его; физические `_rep` и `_dist` служат загрузке и обслуживанию. Соседнюю -домашку витрина источником тоже не берет: витрина слоя DM читает договор -DDS, и задания остаются независимыми. Исключение одно и оно оговорено в -задании 4: у таблиц брака публичного договора нет. +**Писать — в `_dist`.** Так строки распределяются по ключу шардирования. При +записи в локальную таблицу распределение зависит от узла, выполнившего +запрос, и при следующем запуске может измениться. Подробности — раздел +«Раскладка по шардам» [справочника хранилища](../architecture/storage.md). -**Писать — только в `_dist`.** При записи в локальную таблицу раскладку -определяет то, какая нода случайно выполняла код, а не свойство данных. -Правило и его следствия — [дока хранилища](../architecture/storage.md), -«Раскладка по шардам». +**Форма — по зерну.** На стенде агрегат с новым зерном принято хранить +готовым: потребитель читает его много раз без повторного расчета по +источнику. Создайте `dm.<имя>_rep` / `_dist`, донор `dm.<имя>_stage_rep` / +`_stage_dist` и публичное `dm.<имя>_v` с явным списком колонок. При прежнем +зерне достаточно представления: таблица дублировала бы данные. Доводы — +раздел «Форма: по зерну, не по цене запроса» [справочника +DM](../architecture/dm.md). -**Форма — по зерну.** Витрина, меняющая зерно источника, материализуется: -`dm.<имя>_rep` / `_dist`, донор `dm.<имя>_stage_rep` / `_stage_dist` и -публичное `dm.<имя>_v` с явным списком колонок. Витрина, сохраняющая зерно, -остается обычным представлением. Довод — [дока DM](../architecture/dm.md), -«Форма: по зерну, не по цене запроса». +**Ключ шардирования — ваше решение.** Объясните, почему выбранный ключ не +дает существенного перекоса и почему более сложный ничего не добавит. +Объявите донор через `AS <цель>_rep` с ключом цели: иначе замена партиции +потеряет строку на одном шарде и удвоит на другом. Детали — раздел +«Раскладка по шардам» [справочника хранилища](../architecture/storage.md). +Пример обоснования — `dm.revenue_daily_dist` в +[`sql/ddl/55-dm-tables.sql`](../../sql/ddl/55-dm-tables.sql). -**Ключ шардирования — ваше решение.** Ни одно ТЗ ключа не называет. -Правило выбора и требование к донору — [дока -хранилища](../architecture/storage.md), «Раскладка по шардам»: донор -объявляется через `AS <цель>_rep` и шардируется тем же ключом, что цель, -иначе замена партиции потеряет строку на одном шарде и задвоит на другом. -Названный ключ надо уметь защитить: почему он не дает перекоса и почему -более сложный тут ничего не добавляет. Образец такой защиты — -`dm.revenue_daily_dist` в [`sql/ddl/55-dm-tables.sql`](../../sql/ddl/55-dm-tables.sql). +**Где лежит код.** DDL заданий — в `sql/ddl/70-labs.sql`: он применяется +после стендовых, поэтому ваши объекты не мешаются со стендовыми. В +`sql/dm/<витрина>_scope.sql` определяются дни прогона, в `_rebuild.sql` они +собираются в донор, в `_replace.sql` заменяется партиция. Задачу сборки +добавьте в [`dags/dm_transform.py`](../../dags/dm_transform.py): без нее +конвейер не обновит витрину. Представлению достаточно `CREATE VIEW`. -**Где лежит код.** DDL домашек — свой файл `sql/ddl/70-labs.sql`: он -применяется после стендовых по порядку имени, и ваши объекты не мешаются -со стендовыми. У агрегатов рядом со стендовыми ложатся еще три запроса — -`sql/dm/<витрина>_scope.sql` спрашивает у хранилища, какие дни пересобирает -запуск, `_rebuild.sql` собирает их в донор, `_replace.sql` заменяет -партицию, — и задача в -[`dags/dm_transform.py`](../../dags/dm_transform.py). У разминочного -представления нет ничего из этого: весь его код — одно `CREATE VIEW`. - -**Образец на все случаи** — витрина `revenue_daily`: у нее есть все пять -объектов, донор, сборка, замена дневных партиций и задача в конвейере. -Списывать с нее устройство можно и нужно; списывать состав колонок — нет, -он у каждой витрины свой. +**Образец — витрина `revenue_daily`.** У нее есть пять объектов, донор, +сборка, замена дневных партиций и задача в конвейере. Используйте ее как +образец всего порядка пересборки. Состав колонок берите из своего ТЗ, иначе +получите показатели другой витрины. ## Самопроверка ступени 1 -Даг `homework_check` запускается рукой, расписания у него нет. Задача +Даг `homework_check` запускается руками, расписания у него нет. Задача витрины красная, пока витрины нет или счет не сходится, и зеленая, когда -собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет: -это инструмент лабы, а не история качества стенда. +собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет: это +инструмент лабы, а не история качества стенда. Проверка пяти заданий по витринам устроена одинаково и золотых констант не содержит: рядом с витриной независимым запросом собирается тот же ответ — -прямо из источника витрины, минуя ее саму. У четырех заданий источник эталона -тот же, что у витрины, `dds.session_v` или `dds.event_v`; у задания 4 это -таблицы брака ODS, потому что в DDS у брака представительства нет. Обе стороны -соединяются по деловому ключу через `FULL OUTER JOIN`, и считается число строк, -где стороны разошлись или одной стороны нет. Ноль — зеленый. -Полное соединение здесь обязательно: при обычном пропавший день или -пропавший ключ просто не попал бы в результат и остался бы незамечен. +прямо из источника витрины, минуя ее саму. У четырех заданий источник +эталона тот же, что у витрины, `dds.session_v` или `dds.event_v`; у задания +4 это таблицы брака ODS, потому что в DDS нет сущности брака. Обе стороны +соединяются по деловому ключу через `FULL OUTER JOIN`, и считается число +строк, где стороны разошлись или одной стороны нет. Ноль — зеленый. Полное +соединение сохраняет ключи обеих сторон: при внутреннем пропавший день или +ключ не попал бы в результат, и потеря осталась бы незамеченной. Раздел «Как проверить себя» каждого ТЗ по витрине называет деловой ключ и сравниваемые величины. Тот же прием в стендовом исполнении — [даг @@ -105,11 +101,10 @@ DDS, и задания остаются независимыми. Исключе **Предупреждение.** Запросы самой проверки лежат в `sql/dq/` открыто, и имена называют витрину и роль сверки. Проверка на инвариантах неизбежно -близка к ответу: чтобы сверить витрину, надо ее пересчитать. Прятать эти -файлы никто не станет — но открыв их до того, как задание сделано, вы -обменяете урок на готовый SQL. -Смотреть туда стоит после своей сборки: сравнить два решения полезно, -списать одно — нет. +близка к ответу: чтобы сверить витрину, надо ее пересчитать. Если открыть +эти файлы до выполнения задания, можно скопировать готовый SQL, не +разобравшись в расчете самостоятельно. Смотреть туда стоит после своей +сборки: сравнить два решения полезно, списать одно — нет. ## Факультативы diff --git a/docs/labs/dq-errors-daily.md b/docs/labs/dq-errors-daily.md index 878b017..13e2bff 100644 --- a/docs/labs/dq-errors-daily.md +++ b/docs/labs/dq-errors-daily.md @@ -1,118 +1,113 @@ # Задание 4: `dm.dq_errors_daily` -**Чему учит:** у брака свой день и свой срок жизни. Витрина качества живет -по времени загрузки, а не по модельному дню, и переживает собственный -источник — поэтому «пересобрать все» здесь означает «стереть историю». +**Чему учит:** считать брак по дню загрузки и сохранять сводку дольше +исходных строк. Пересборка всех партиций после удаления источника по `TTL` +стирает историю, которую уже не восстановить. + +## Сначала прочитать + +- [«Хранилище и его слои»](../handbook/storage-layers.md); +- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). ## Роль витрины -Сводка приемки: сколько строк за день не прошли разбор и по какой причине. -Читают ее, когда качество потока просело: сначала смотрят, вырос ли класс -брака, потом идут за сырым текстом в саму таблицу ошибок. +Сводку открывают при росте брака в потоке: смотрят, каких ошибок стало +больше и сколько строк забраковано. Причину конкретной ошибки исследуют +в `ods.event_errors_dist` и `ods.order_errors_dist`, где есть сырой текст и +координаты доставки. -Витрина отвечает на вопрос «сколько и чего», а не «что именно сломалось». -Разбирательство с конкретной строкой начинается в `ods.event_errors_dist` и -`ods.order_errors_dist`, где рядом с классом лежат сырой текст и координаты -доставки. +## Что сделать -## Источник: единственное исключение из правила `_v` +Зерно — **день загрузки × источник × класс брака**. В источнике зерно — +строка брака. Зерно новое, поэтому витрина хранится готовым агрегатом — см. +[«Как принято на ступени 1»](README.md#как-принято-на-ступени-1). Создайте +пять объектов: `dm.dq_errors_daily_rep` / `_dist`, донор `_stage_rep` / +`_stage_dist` и публичное представление `dm.dq_errors_daily_v`. Ключ +партиции — день загрузки. -Общее правило домашек — читать через публичный договор `_v`. У таблиц брака -такого договора нет, и заводить его ради этой витрины не надо: брак — не -сущность бизнес-модели, а диагностика слоя приема. Читать нужно -распределенные таблицы напрямую: +### Состав колонок -- `ods.event_errors_dist` — брак разбора событий; -- `ods.order_errors_dist` — брак разбора слепка заказов. - -Именно `_dist`, а не `_rep`: локальная таблица вернет строки одного шарда, и -витрина тихо покажет половину брака. Ошибки при этом не будет — ошибка -появится позже, когда по витрине примут решение. - -## Зерно и форма - -Зерно — **день загрузки × источник × класс брака**. Зерно источника — -строка брака, значит зерно сменилось и витрина материализуется: -`dm.dq_errors_daily_rep` / `_dist`, донор `_stage_rep` / `_stage_dist`, -публичное `dm.dq_errors_daily_v`. Ключ партиции — день загрузки. - -День здесь — `toDate(_load_ts)`, то есть настоящее время, когда строка -приехала в хранилище, а не модельный день мира. Модельного дня у брака нет -и быть не может: он живет в содержимом, а содержимое как раз и не -разобралось. - -## Состав колонок - -| Колонка | Тип | Что это | +| Колонка | Тип | Значение | |---|---|---| -| `load_date` | `Date` | день загрузки, в который приехал брак | +| `load_date` | `Date` | День загрузки забракованной строки | | `source` | `LowCardinality(String)` | `event` или `order` | -| `error_class` | `LowCardinality(String)` | класс брака из таблицы ошибок | -| `error_rows` | `UInt64` | число забракованных строк | -| `_load_id` | `String` | запуск, собравший день | -| `_load_ts` | `DateTime64(3, 'UTC')` | когда собран | +| `error_class` | `LowCardinality(String)` | Класс брака из таблицы ошибок | +| `error_rows` | `UInt64` | Число забракованных строк | +| `_load_id` | `String` | Идентификатор запуска, собравшего день | +| `_load_ts` | `DateTime64(3, 'UTC')` | Время сборки | -Две похожие колонки рядом различаются намеренно: `load_date` — про день, -когда приехал брак, `_load_ts` — про запуск, который собрал витрину. Первая -деловая, вторая служебная, и совпадать они не обязаны. +Вычислите `load_date` как `toDate(_load_ts)` из таблицы ошибок — это +реальный день приема строки. Модельный день записан внутри исходной строки. +Ее не удалось разобрать, поэтому надежно получить из нее день не получится. +Служебная `_load_ts` витрины описывает сборку сводки; ее дата может +отличаться от `load_date`. -Классы у двух источников свои и не пересекаются по смыслу, поэтому колонка -`source` в зерне обязательна: без нее одинаковое имя класса у событий и у -заказов слилось бы в одну строку. +У событий и заказов свои классы брака. `source` входит в зерно, чтобы +одинаковые имена классов с разным смыслом не объединялись в одну строку. -## Ритм пересборки: витрина переживает источник +### Ритм пересборки -Таблицы брака живут месяц (`TTL`), сырые данные — трое суток. Витрина живет без -срока: сводка «сколько брака было в марте» нужна и после того, как сами строки -марта удалены. +Брак хранится месяц по `TTL`, сырые данные — трое суток. Сводка хранится +бессрочно: итоги нужны и после удаления исходных строк. -Отсюда правило прогона: пересобираются **только те дни загрузки, которые -еще есть в источнике**. Спросите у источника список живых дней и замените -партиции ровно этих дней. Соблазн «собрать все заново в донор и подменить -таблицу целиком» здесь стоит дороже обычного: истекшие по `TTL` дни исчезли -бы из витрины навсегда, а вместе с ними — единственная оставшаяся память о -них. +Получите список дней загрузки, еще имеющихся в источниках. Соберите их в +донор и замените только эти партиции, сохранив остальные. -Текущий день загрузки не замерзает до конца суток: поток событий идет, и -брак в него еще добавится. Это нормально, день просто пересоберется на +В течение текущих суток ошибки еще поступают: пересчитайте этот день на следующем прогоне. -## Ключ шардирования +### Ключ шардирования -Ваше решение по [правилу донора](../architecture/storage.md), «Раскладка по -шардам». Витрина крошечная: классов брака у событий и заказов вместе меньше -десятка. Учтите это в защите ключа — иногда правильный ответ звучит как -«перекос здесь ничего не стоит, и вот почему». +Выберите ключ самостоятельно. Классов брака у событий и заказов вместе +меньше десятка. Объясните, насколько существенен перекос при таком объеме. + +Ключ донора — тот же, что у цели (см. [«Как принято на ступени +1»](README.md#как-принято-на-ступени-1) и раздел «Раскладка по шардам» +[справочника хранилища](../architecture/storage.md)). + +## Источники: таблицы `_dist` + +Только это задание читает данные не через `_v`: брак нужен для диагностики +приема и не входит в бизнес-модель. Публичных представлений у таблиц ошибок +нет, создавать их для задания не нужно. Источники: + +- `ods.event_errors_dist` — ошибки разбора событий; +- `ods.order_errors_dist` — ошибки разбора слепка заказов. + +Чтение локальной `_rep` даст строки одного шарда из двух: часть брака не +попадет в сводку, хотя запрос выполнится без ошибки. + +## Типичная ошибка + +Если заменить всю таблицу данными из нового донора, исчезнут итоги дней, +истекших по `TTL` источника. Восстановить их неоткуда. ## Как проверить себя -Деловой ключ — пара `source` и `error_class` внутри `load_date`. Область -проверки — дни загрузки, которые еще живы хотя бы в одной таблице ошибок: -день за границей `TTL` сверять не с чем, и его отсутствие в источнике не -ошибка. +Деловой ключ — `source` и `error_class` внутри `load_date`. Проверяйте дни, +оставшиеся хотя бы в одной таблице ошибок. Если дня за пределами `TTL` уже +нет в источнике, это не ошибка: сверять его уже не с чем. -Соберите прямой счет по обеим таблицам ошибок, соедините с -`dm.dq_errors_daily_v` полным внешним соединением по ключу и посчитайте -строки, где ключа нет с одной из сторон или где расходится `error_rows`. -Ноль — зеленый. +Посчитайте строки прямо в обеих таблицах ошибок и соедините результат с +`dm.dq_errors_daily_v` полным внешним соединением по ключу. Затем посчитайте +строки, где ключ отсутствует с одной из сторон или различается `error_rows`. +Расхождений должно быть ноль. -Отдельно: сумма `error_rows` по живым дням равна общему числу строк в -`ods.event_errors_dist` плюс `ods.order_errors_dist`. Если эта сумма вышла -примерно вдвое меньше — витрина почти наверняка собрана с `_rep` вместо -`_dist` и видит один шард из двух. +Сумма `error_rows` по проверяемым дням равна общему числу строк в +`ods.event_errors_dist` и `ods.order_errors_dist`. Если она примерно вдвое +меньше, вероятная причина — чтение `_rep` вместо `_dist`. -**Пустая витрина — верный ответ.** На чистом стенде брака нет, обе стороны -пусты, расхождений ноль, задача зеленая. Проверка, которая требует хотя бы -одной строки, на здоровом стенде краснела бы всегда и приучала бы себя -игнорировать. +Пустая витрина на чистом стенде — верный ответ: обе стороны пусты, +расхождений ноль, проверка успешна. Требование хотя бы одной строки дало бы +постоянную ошибку, которую со временем стали бы игнорировать. ## Приемка -- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется - ключом цели; -- [ ] источники — `_dist`-таблицы обоих видов брака; -- [ ] прогон заменяет партиции только живых дней источника и не трогает - истекшие; -- [ ] задача `dq_errors_daily` в `homework_check` зеленая — в том числе на - чистом стенде, где брака нет; +- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом + цели; +- [ ] источники — таблицы `_dist` обоих видов брака; +- [ ] прогон заменяет партиции только дней, еще имеющихся в источниках, и + сохраняет дни за пределами их срока хранения; +- [ ] задача `dq_errors_daily` в `homework_check` завершилась успешно, в том + числе на чистом стенде без брака; - [ ] задача витрины добавлена в `dm_transform`. diff --git a/docs/labs/events-enriched.md b/docs/labs/events-enriched.md index 2c20ca7..2a9587f 100644 --- a/docs/labs/events-enriched.md +++ b/docs/labs/events-enriched.md @@ -1,114 +1,93 @@ # Задание 1: `dm.events_enriched_v` -**Чему учит:** слой — это контракт, а не копия данных. Витрина, которая не -меняет зерно источника, остается представлением, и копии событий ради -других имен колонок не заводит ни один боевой склад. +**Чему учит:** строить слой как договор о данных, а не как их копию. +Оставлять витрину представлением, если зерно источника не меняется. -Это разминка. Строк она не хранит, в конвейер не встает, ключа шардирования -у нее нет. Задача одна: собрать плоское событие для BI и не поддаться -соблазну разложить его на диск. +## Сначала прочитать + +- [«Хранилище и его слои»](../handbook/storage-layers.md). ## Роль витрины -Договор события в DDS широкий — 44 колонки, почти весь формат выгрузки: -часть из них технические координаты источника, часть — параллельные массивы -товаров. Аналитику в Superset нужен плоский набор деловых полей и пара -производных, которые иначе каждый пишет заново в каждом запросе. +В `dds.event_v` 44 колонки, включая технические поля источника и массивы +товаров. Для анализа событий в Superset нужны деловые поля и три готовых +значения: час события, путь страницы и домен перехода. Тогда вам не придется +повторять эти выражения в каждом запросе. -Витрина отвечает на вопрос «что вообще происходило»: с нее начинают, когда -надо посмотреть на события глазами человека, а не разбирать широкую строку -источника. +Витрины следующих заданий читают DDS напрямую. Использовать +`dm.events_enriched_v` как источник для них не нужно: задания независимы, их +можно выполнять в любом порядке. -Источником для витрин следующих заданий она не служит. Витрина слоя DM -читает публичный договор DDS — так устроены и стендовые витрины, и потому -задания независимы: соседнюю домашку можно делать первой, а можно не делать -вовсе. +## Что сделать -## Зерно и форма +Создайте обычное представление `CREATE VIEW dm.events_enriched_v`. +Единственный источник — `dds.event_v`. Зерно — **событие**: каждой строке +источника соответствует одна строка витрины. -Зерно — **событие**: одна строка витрины равна одной строке `dds.event_v`. -Зерно источника сохранено, значит форма — обычное представление -`CREATE VIEW dm.events_enriched_v`, без физической таблицы, без донора и -без задачи в `dm_transform`. Пересобирать нечего. +Физическая таблица, таблица-донор и задача в `dm_transform` не нужны. +Представление не хранит строки, поэтому ключ шардирования и пересборка ему +не требуются. -Источник — `dds.event_v`, и только он. +Укажите колонки явно. Иначе новая колонка источника попадет в витрину +автоматически, и договор с потребителем изменится без вашего участия. -## Состав колонок +| Группа | Колонки | +|---|---| +| Время и посетитель | `event_date`, `event_time`, `event_hour`, `client_id`, `event_id` | +| Действие | `event_type` (`pageview`, `add_to_cart`, `purchase`), `url`, `page_path`, `referer`, `referer_host`, `title` | +| UTM-метки | `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term` | +| Устройство и регион | `device_category`, `browser`, `operating_system`, `region_country`, `region_city` | +| Товары и покупка | `product_id`, `product_name`, `product_category`, `product_price`, `product_quantity`, `product_event_type`, `purchase_id`, `purchase_revenue`, `purchase_coupon` | -Явный список обязателен: договор не должен расширяться сам, когда в -источник добавят колонку. +Три колонки вычислите в представлении. Остальные возьмите из источника без +изменений, сохранив массивы `product_*`. -**Время и посетитель:** `event_date`, `event_time`, `event_hour`, -`client_id`, `event_id`. - -**Действие:** `event_type` (`pageview`, `add_to_cart`, `purchase`), `url`, -`page_path`, `referer`, `referer_host`, `title`. - -**Атрибуция:** `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, -`utm_term`. - -**Паспорт визита:** `device_category`, `browser`, `operating_system`, -`region_country`, `region_city`. - -**Торговый блок, массивами как в источнике:** `product_id`, `product_name`, -`product_category`, `product_price`, `product_quantity`, -`product_event_type`, `purchase_id`, `purchase_revenue`, `purchase_coupon`. - -Три колонки в источнике не лежат и считаются здесь: - -| Колонка | Как считается | Зачем | +| Колонка | Как считается | Для чего нужна | |---|---|---| -| `event_hour` | `toHour(event_time)` | суточный профиль спрашивают в каждом отчете | -| `page_path` | путь `url` без домена и параметров | группировка по странице без разбора строки в каждом запросе | -| `referer_host` | домен `referer` | «откуда пришли» одним значением, а не полным адресом | +| `event_hour` | `toHour(event_time)` | Анализ событий по часам суток | +| `page_path` | путь `url` без домена и параметров | Группировка по странице | +| `referer_host` | домен `referer` | Анализ источников перехода | -Служебных `_load_id` и `_load_ts` у витрины нет, и это не забывчивость. Их -нет и у `dds.event_v`: событие в DDS не грузят, а читают из ODS, поэтому -координат собственной загрузки у него не бывает. Служебная колонка называет -запуск, который положил строку на диск, — здесь на диск никто ничего не -кладет. +Не добавляйте `_load_id` и `_load_ts`. Их нет и у `dds.event_v`: события в +DDS читают из ODS без отдельной загрузки. Эти служебные поля указывают на +запуск, который записал строку на диск. Представление ничего не загружает. -## Ловушка задания +## Типичная ошибка -Массивы `product_*` тянет развернуть через `ARRAY JOIN` — так плоская -таблица становится по-настоящему плоской. Разворачивать нельзя, и вреда -здесь два. +Не разворачивайте массивы товаров через `ARRAY JOIN`. У события с тремя +позициями получится три строки. Зерно изменится. Витрину с новым зерном на +стенде хранят готовым агрегатом — причины описаны в разделе [«Как принято на +ступени 1»](README.md#как-принято-на-ступени-1). -Очевидный: у события с тремя позициями получилось бы три строки, зерно -витрины перестало бы совпадать с зерном источника, а вместе с ним развалилось -бы и правило формы — такую витрину уже пришлось бы материализовать. +Главная опасность — потеря событий с пустым массивом товаров: `ARRAY JOIN` +исключает их из результата. Товары есть только в событиях корзины и покупки. +Поэтому исчезнет большинство событий — девятнадцать строк из двадцати. +Запрос выполнится без ошибки, а витрина по имени и составу колонок +по-прежнему будет выглядеть таблицей событий. -Неочевидный и куда более разрушительный: `ARRAY JOIN` — соединение -внутреннее, и строку с пустым массивом он выбрасывает. Торговый блок пуст у -всех событий, кроме корзин и покупок, то есть у подавляющего большинства. -Так «плоская таблица событий» превращается в таблицу товарных позиций и -теряет девятнадцать строк из двадцати — а по имени и по составу колонок она -по-прежнему выглядит таблицей событий. Ошибка не падает и ничего не -подсвечивает. - -Массивы едут в договор как есть; разворачивает их тот, кому нужна позиция -заказа, — и делает это уже в своей витрине, с другим зерном и другим именем. +Сохраните массивы как в источнике. Для анализа отдельных товарных позиций +нужна другая витрина с другим зерном и именем. ## Как проверить себя -Деловой ключ — `event_id` внутри дня. Соберите прямой ответ из -`dds.event_v` за тот же день, соедините с витриной полным внешним -соединением и посчитайте строки, где ключа нет с одной из сторон или где -различаются значения деловых колонок. Ноль — зеленый. +Деловой ключ — `event_id` внутри дня. Получите результат прямо из +`dds.event_v` за тот же день. Соедините его с витриной полным внешним +соединением по `event_id`. Посчитайте строки, где ключ отсутствует с одной +из сторон или различаются значения деловых колонок. Расхождений должно быть +ноль. -Отдельно проверяются три производные колонки: `event_hour`, `page_path` и -`referer_host` должны совпасть с тем же выражением, посчитанным по -`dds.event_v` на лету. +Отдельно сверьте `event_hour`, `page_path` и `referer_host` с теми же +выражениями, рассчитанными прямо по `dds.event_v`. -Если строк в витрине оказалось в разы меньше, чем в источнике, а остались в -ней одни корзины и покупки — это развернутые массивы: `ARRAY JOIN` выбросил -все события с пустым торговым блоком. +Если строк в витрине в разы меньше, чем в источнике, и остались только +события корзины и покупки, это признак развернутых массивов: `ARRAY JOIN` +исключил все события с пустым массивом товаров. ## Приемка -- [ ] `dm.events_enriched_v` создано, физических таблиц под ним нет; -- [ ] список колонок в представлении явный; +- [ ] представление `dm.events_enriched_v` создано без физических таблиц; +- [ ] список колонок в представлении задан явно; - [ ] число строк и набор `event_id` совпадают с `dds.event_v` за каждый прожитый день; -- [ ] задача `events_enriched` в `homework_check` зеленая; -- [ ] задачи в `dm_transform` **нет** — и не должно быть. +- [ ] задача `events_enriched` в `homework_check` завершилась успешно; +- [ ] задачи для этой витрины в `dm_transform` нет. diff --git a/docs/labs/session-overview.md b/docs/labs/session-overview.md index 9cc5c0b..c2d2961 100644 --- a/docs/labs/session-overview.md +++ b/docs/labs/session-overview.md @@ -1,121 +1,118 @@ # Задание 3: `dm.session_overview` -**Чему учит:** агрегат хранит слагаемые, а не готовые средние и доли. -Среднее по строкам витрины не складывается никогда, а уникальных нельзя -складывать даже тогда, когда на сегодняшних данных сумма сходится. +**Чему учит:** хранить слагаемые, а не готовые средние и доли. Понимать, +почему сумма `visitors` по строкам сейчас сходится, но начнет завышать +аудиторию, если у одной куки появятся визиты с двух устройств. + +## Сначала прочитать + +- [«Хранилище и его слои»](../handbook/storage-layers.md); +- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). ## Роль витрины -Дневной портрет визитов в разрезе устройств: сколько было сессий, какие они -были длинные, сколько страниц смотрели и как часто доходили до покупки. -Вопрос витрины — «как люди ходят по магазину с телефона и с настольного -компьютера, и чем эти визиты отличаются». +Витрина нужна, чтобы по дням сравнивать визиты с телефона и компьютера: их +число, длительность, просмотры страниц и частоту покупок. -Сессия на стенде — расчетная сущность, ее уже нарезал слой DDS. Собирать -визиты заново из событий не нужно и нельзя: это была бы вторая нарезка, -которая тихо разойдется с первой. +На прошлом стенде витрина выделяла визиты сама. Теперь их рассчитывают в +DDS, а повторять зерно сессий ради представления с переименованными +колонками незачем. Здесь вы учитесь дневной агрегации, поэтому зерно — день +и устройство. Выбор описан в разделе «Форма: по зерну, не по цене запроса» +[справочника DM](../architecture/dm.md). -Витрина с таким именем была и на прошлом стенде, но зерно у нее сменилось, -и сменилось не по вкусу. Там визитов как сущности не было, и витрина -нарезала их сама, строкой на визит. Здесь визит — сущность слоя DDS, и -витрина, повторяющая его зерно, оказалась бы обычным представлением по -[правилу формы](../architecture/dm.md): копией сессий с другими именами -колонок. Чтобы задание осталось про дневной агрегат, витрина поднялась на -зерно выше — до дня и среза. +## Что сделать -## Зерно и форма +Источник — `dds.session_v`. Зерно — **день сессии × категория устройства**. +Зерно новое, поэтому витрина хранится готовым агрегатом — см. [«Как принято +на ступени 1»](README.md#как-принято-на-ступени-1). Создайте пять объектов: +`dm.session_overview_rep` / `_dist`, донор `_stage_rep` / `_stage_dist` и +публичное представление `dm.session_overview_v`. Ключ партиции — день. -Зерно — **день сессии × категория устройства**. Источник — -`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')` | когда собран | +| `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`, долю считает тот, кто спрашивает. +Возьмите `device_category` из сессии: DDS уже заменил код словом. Из четырех +категорий встречаются `desktop`, `mobile` и `tablet`. Строку для `tv` +создавайте только при появлении таких визитов. -С `visitors` история тоньше, и в ней главная ловушка задания. Складывать -уникальных по строкам нельзя: один и тот же посетитель, попавший в две -строки, будет посчитан дважды. Проверьте на стенде — и увидите, что сумма -`visitors` по устройствам за день **сходится** с `dm.daily_traffic_v`. +Вместо `avg_duration_seconds` храните слагаемые. Среднее из средних по трем +устройствам даст неверный ответ за день: число сессий в категориях +различается, поэтому средние нужно взвешивать. Потребитель делит сумму +`duration_seconds_total` на сумму `sessions`. Долю отказов считают так же: +сумму `bounce_sessions` делят на сумму `sessions`, а не усредняют готовые +доли. -Это не отменяет правила, а показывает, откуда берется совпадение: в мире -стенда устройство приписано куке на всю жизнь ([словарь -понятий](../../CONTEXT.md), «Паспорт куки»), поэтому одна кука лежит ровно -в одной строке дня, и пересекаться строкам не в чем. +### Ритм пересборки -Совпадение это — свойство мира, а не свойство витрины. Появятся визиты -одной куки с двух устройств — сумма поедет, и ни одна проверка об этом не -скажет: отчет просто начнет тихо завышать аудиторию. Отсюда правило, -которое стоит унести с задания: складывать уникальных нельзя даже там, где -сегодня получается. Число посетителей дня спрашивают у -`dm.daily_traffic_v`, где оно посчитано на своем зерне. +Сессии прожитого дня больше не меняются. Как в задании 2, определяйте +диапазон от первого несобранного прожитого дня до последнего прожитого. +Соберите его в донор и замените дневные партиции. Добавьте задачу в +`dm_transform`. -## Ритм пересборки +### Ключ шардирования -Источник — сессии: день замерзает, как только прожит. Объем прогона тот же, -что в задании 2, — хвост от первого несобранного прожитого дня; механика та -же: донор и замена дневных партиций. +Выберите ключ самостоятельно. Визитов с планшета мало, с телефонов и +компьютеров — значительно больше. Если хотите взять хеш только от +`device_category`, сначала посчитайте строки на каждом шарде. Затем +объясните, почему такое распределение вас устраивает. -## Ключ шардирования +Ключ донора — тот же, что у цели (см. [«Как принято на ступени +1»](README.md#как-принято-на-ступени-1) и раздел «Раскладка по шардам» +[справочника хранилища](../architecture/storage.md)). -Ваше решение по [правилу донора](../architecture/storage.md), «Раскладка по -шардам». Здесь у измерения всего три встречающихся значения, и распределены -они неровно: планшеты — тонкая полоса, а телефоны и настольные машины держат -основную массу. Прежде чем брать хеш от одного `device_category`, посчитайте, -сколько строк ляжет на каждый шард, и назовите, чем вас устраивает результат. +## Почему сумма `visitors` сходится с `dm.daily_traffic_v` + +Сравните сумму `visitors` по устройствам за день с `dm.daily_traffic_v`. На +стенде значения совпадут: устройство приписано куке на всю жизнь, поэтому за +день она учитывается только в одной строке. Это свойство мира стенда; см. +раздел «Паспорт куки» в [словаре понятий](../../CONTEXT.md). + +Если появятся визиты одной куки с двух устройств, сумма начнет завышать +аудиторию. Проверки счетчиков по категориям этого не обнаружат. Число +посетителей дня берите из `dm.daily_traffic_v`, где оно рассчитано для дня +целиком. + +## Типичная ошибка + +Повторное выделение визитов из событий может дать другие границы сессий, чем +в DDS. Используйте готовые сессии из `dds.session_v`. Если общее число +событий совпало с источником, а число сессий завышено, вероятная причина — +сборка витрины из событий вместо сессий. ## Как проверить себя -Деловой ключ — `device_category` внутри дня. Соберите прямой агрегат из -`dds.session_v` за день, соедините с `dm.session_overview_v` полным внешним -соединением и посчитайте строки, где ключа нет с одной из сторон или где -расходится любой счетчик. Ноль — зеленый. +Деловой ключ — `device_category` внутри дня. Получите прямой агрегат из +`dds.session_v` за день. Соедините его с `dm.session_overview_v` полным +внешним соединением по ключу. Посчитайте строки, где ключ отсутствует с +одной из сторон или различается любой счетчик. Расхождений должно быть ноль. -Две проверки покрупнее ловят грубую ошибку сразу: +Еще две проверки дневных итогов: -- сумма `sessions` за день равна числу строк `dds.session_v` этого дня: - ни один визит не потерян и ни один не задвоен; -- сумма `events_total` за день равна сумме `events_total` по сессиям того - же дня. - -Если суммы событий сходятся, а сумма сессий больше числа визитов — почти -наверняка витрина собрана из событий, а не из сессий. +- сумма `sessions` равна числу строк `dds.session_v` за день — без потерь и + повторного учета визитов; +- сумма `events_total` равна сумме `events_total` по сессиям того же дня. ## Приемка -- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется - ключом цели; -- [ ] `dm.session_overview_v` перечисляет колонки явно; -- [ ] источник витрины — `dds.session_v`, своей нарезки визитов в ней нет; -- [ ] средних и долей в физической таблице нет; -- [ ] задача `session_overview` в `homework_check` зеленая; +- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом + цели; +- [ ] в `dm.session_overview_v` колонки перечислены явно; +- [ ] источник — `dds.session_v`, повторного выделения визитов нет; +- [ ] физическая таблица не хранит средние и доли; +- [ ] задача `session_overview` в `homework_check` завершилась успешно; - [ ] задача витрины добавлена в `dm_transform`. diff --git a/docs/labs/top-pages-daily.md b/docs/labs/top-pages-daily.md index 254a3f0..5a1bd41 100644 --- a/docs/labs/top-pages-daily.md +++ b/docs/labs/top-pages-daily.md @@ -1,114 +1,110 @@ # Задание 2: `dm.top_pages_daily` -**Чему учит:** зерно сменилось — значит витрина материализуется, живет -дневными партициями и встает в конвейер. Первая настоящая витрина: пять -объектов, донор, замена партиции, задача в `dm_transform`. +**Чему учит:** создавать витрину с новым зерном: пять объектов, донор и +замена дневных партиций. Включать ее пересборку в `dm_transform`. + +## Сначала прочитать + +- [«Хранилище и его слои»](../handbook/storage-layers.md); +- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). ## Роль витрины -Популярность страниц по дням: сколько раз страницу открыли, сколько разных -посетителей ее видели и что на ней делали кроме просмотра. +По дневным показателям страниц аналитик сравнивает просмотры, посетителей и +действия с товарами. Добавление в корзину происходит на карточке товара, +покупка — на странице подтверждения заказа. Отношение добавлений к +просмотрам карточки показывает ее конверсию. -Торговое событие садится на ту страницу, где случилось: корзина — на -карточку товара, покупка — на страницу подтверждения заказа. Поэтому у -карточки товара рядом стоят просмотры и корзины, и их отношение — -конверсия карточки — главный вопрос к этой витрине: какие товары кладут в -корзину, едва открыв, а какие смотрят и уходят. +Для воронки «просмотр → корзина → покупка» этих данных недостаточно: +действия происходят на разных страницах, а связи между ними в агрегате нет. +Путь человека исследуют по событиям или сессиям. -Воронки «просмотр → корзина → покупка» здесь нет и быть не может: корзина и -покупка живут на разных страницах, и связать их шаги в одну цепочку на -зерне страницы не из чего. Путь одного человека собирают из событий или из -сессий, а витрина страниц отвечает про страницу. +## Что сделать -## Зерно и форма +Источник — `dds.event_v`. Зерно витрины — **день события × страница**. +Читайте DDS напрямую: так выполнение задания не зависит от витрины +задания 1. -Зерно — **день события × страница**. Источник — события, у него зерно -события, значит зерно сменилось и витрина материализуется: -`dm.top_pages_daily_rep` / `_dist`, донор `_stage_rep` / `_stage_dist`, -публичное `dm.top_pages_daily_v`. +На стенде витрину с новым зерном хранят как готовый агрегат, чтобы не +рассчитывать ее заново для каждого запроса. Причины описаны в разделе +[«Как принято на ступени 1»](README.md#как-принято-на-ступени-1). Создайте +пять объектов: +`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')` | когда собран | +| `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` у всех строк -обнулятся. Ошибка тихая: витрина выглядит собранной, и главный ее вопрос — -конверсия карточки — молча отвечает «ноль везде». +Вычислите `page_path` из `url`, убрав домен и параметры запроса. Полный +адрес разделил бы показатели одной страницы на сотни строк. Учитывайте все +три типа событий, а их счетчики вычисляйте через `countIf`. -## Ритм пересборки +### Ритм пересборки -Источник — события: день замерзает сразу, как только прожит. Значит -пересобирать нужно только то, что еще не собрано: хвост дней, которых в -витрине нет, — от первого несобранного прожитого дня до последнего -прожитого. Готовые дни трогать незачем, а вот дыру, оставленную частичным -отказом, объем прогона обязан подобрать: ищите не последний собранный день, а -первый несобранный. +События прожитого дня больше не меняются. Определяйте объем прогона по +состоянию витрины: от первого несобранного прожитого дня до последнего +прожитого. Отсчет от последнего собранного дня оставит пропуски после +частичного сбоя. -Механика — сборка отрезка в донор одной вставкой и замена дневных партиций -по одной, как у `revenue_daily`. +Как у `revenue_daily`, заполните донор за весь диапазон одной вставкой, +затем замените дневные партиции по одной. -## Ключ шардирования +### Ключ шардирования -Ваше решение по [правилу донора](../architecture/storage.md), «Раскладка по -шардам». Подумайте, чем эта витрина отличается от `revenue_daily`: там кроме -дня было одно измерение из шести близких по объему значений, здесь — -страницы, которых много и которые по популярности очень неровные. +Выберите ключ самостоятельно. У `revenue_daily` кроме дня одно измерение с +шестью близкими по объему значениями. Здесь страниц много, и число событий +на них сильно различается. + +У цели и донора используйте одинаковый ключ: иначе при замене партиции +строка потеряется на одном шарде и удвоится на другом. О том, [как устроен +донор](../architecture/storage.md), читайте в разделе «Раскладка по шардам». + +## Типичная ошибка + +Из-за имени витрины хочется отобрать только просмотры. Но фильтр источника +по `event_type = 'pageview'` обнулит `cart_adds` и `purchases`, а запрос не +завершится ошибкой. Конверсия карточек станет нулевой. Проверка одной суммы +`pageviews` не обнаружит этот фильтр: все просмотры остались на месте. +Ошибку покажет сверка `purchases` или `cart_adds` с источником, если за день +были покупки или добавления в корзину. + +Выбирайте проверку под возможную ошибку. Этот подход описан в +[справочнике проверок](../architecture/testing.md). ## Как проверить себя -Деловой ключ — `page_path` внутри дня. Соберите прямой агрегат из -`dds.event_v` за день, соедините с `dm.top_pages_daily_v` полным внешним -соединением по ключу и посчитайте строки, где ключа нет с одной из сторон или -где расходится любой из четырех счетчиков. Ноль — зеленый. +Деловой ключ — `page_path` внутри дня. Получите прямой агрегат из +`dds.event_v` за день и соедините его с `dm.top_pages_daily_v` полным +внешним соединением по ключу. Посчитайте строки, где ключ отсутствует с +одной из сторон или различается любой из четырех счетчиков. Расхождений +должно быть ноль. -Две проверки попроще, ими удобно ловить грубую ошибку до полного -соединения: +Перед полным сравнением проверьте дневные суммы: -- сумма `pageviews` за день равна числу событий `pageview` этого дня в - `dds.event_v`; -- сумма `purchases` за день равна числу событий `purchase` этого дня. +- сумма `pageviews` равна числу событий `pageview` в `dds.event_v`; +- сумма `purchases` равна числу событий `purchase` в том же источнике. -Проверки ловят разное. Фильтр `WHERE event_type = 'pageview'` первая из них -пропускает: отбор строк по типу не меняет счет событий того же типа. -Расхождение покажет только вторая — вместе с `cart_adds` обнулятся и -`purchases`. Проверку выбирают по тому, умеет ли она покраснеть на вашей -ошибке, а не по тому, как быстро она пишется: [проверка, которая не умеет -краснеть, бесполезна](../architecture/testing.md). - -`visitors` складывать по строкам нельзя: один посетитель ходит по разным -страницам и в сумме посчитается несколько раз. Число посетителей дня -спрашивают у `dm.daily_traffic_v`, а не у этой витрины. +Сумма `visitors` по страницам повторно учитывает посетителя, открывшего +несколько страниц. Число посетителей за день берите из `dm.daily_traffic_v`. ## Приемка -- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется - ключом цели; -- [ ] `dm.top_pages_daily_v` перечисляет колонки явно, включая `_load_id` и - `_load_ts`; -- [ ] объем прогона выводится из состояния витрины, а не задается руками; -- [ ] задача `top_pages_daily` в `homework_check` зеленая; +- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом + цели; +- [ ] в `dm.top_pages_daily_v` явно перечислены колонки, включая `_load_id` + и `_load_ts`; +- [ ] объем прогона определяется по состоянию витрины без ручного задания; +- [ ] задача `top_pages_daily` в `homework_check` завершилась успешно; - [ ] задача витрины добавлена в `dm_transform`, повторный прогон конвейера - результат не меняет. + не меняет результат. diff --git a/docs/labs/utm-effectiveness.md b/docs/labs/utm-effectiveness.md index 3b5d467..3853828 100644 --- a/docs/labs/utm-effectiveness.md +++ b/docs/labs/utm-effectiveness.md @@ -1,150 +1,138 @@ # Задание 5: `dm.utm_effectiveness` -**Чему учит:** приставка `declared_` в имени колонки — предупреждение, а не -украшение. Она говорит: это заявка клиента, а не деньги отчетности. Урок -здесь дает конфликт: суммы не сойдутся, и раскапывать почему предстоит -самостоятельно. +**Чему учит:** понимать предупреждение в приставке `declared_`: это сумма со +слов клиента, а не деньги отчетности. Самостоятельно находить причины +расхождения двух сумм выручки. -Правило стенда: **поведение и атрибуцию считаем по трекеру, деньги — по -бэкенду**. Эта витрина — единственное разрешенное исключение: атрибуции по -UTM у бэкенда нет, метки живут только в кликстриме. Раз считать иначе -нечем, клиентская сумма берется, но называется своим именем. +## Сначала прочитать + +- [«Хранилище и его слои»](../handbook/storage-layers.md); +- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). ## Роль витрины -Эффективность рекламных каналов по дням: сколько трафика привел канал, -сколько корзин и покупок в нем случилось и на какую сумму эти покупки, по -словам клиента. +Аналитик сравнивает рекламные каналы по дням: трафик, добавления в корзину, +покупки и заявленные клиентом суммы. -## Атрибуция: по событию, а не по визиту +На стенде поведение и атрибуцию считают по трекеру, деньги — по бэкенду. +Здесь сделано исключение: UTM-метки есть только в кликстриме, поэтому для +оценки покупок по меткам используют клиентскую сумму. -Метки едут на каждом событии, и витрина группирует события по их -собственным меткам. Модели «первое касание» или «последнее касание» здесь -нет: их построение — отдельная тема, а это витрина трафика по меткам. +## Что сделать -`dds.session_v` при этом несет UTM первого события визита, и ответ «какой -канал привел» из сессии и из этой витрины будет разным. Это не -ошибка ни одной из сторон — это два разных вопроса, и в отчете их надо -различать. +Читайте `dds.event_v` напрямую, без соседних витрин DM. Зерно — **день +события × `utm_source` × `utm_medium` × `utm_campaign`**. Зерно новое, +поэтому витрина хранится готовым агрегатом — см. [«Как принято на ступени +1»](README.md#как-принято-на-ступени-1). Создайте пять объектов: +`dm.utm_effectiveness_rep` / `_dist`, донор `_stage_rep` / `_stage_dist` и +публичное представление `dm.utm_effectiveness_v`. Ключ партиции — день. -## Прямой трафик из витрины не выбрасывается +### Состав колонок -Соблазн отфильтровать строки без меток велик: витрина же про рекламу. -Отфильтровать нельзя по двум причинам. Первая деловая: прямой трафик — -обычно самый крупный «канал» магазина, и отчет, где его нет, врет о доле -рекламы. Вторая техническая: как только часть событий выпала, итог витрины -перестал сходиться с итогом источника, и проверить сборку стало нечем. - -Пустые метки остаются пустыми строками, как в источнике. Своего слова для -«прямого захода» витрина не изобретает: чем меньше выдуманных значений, тем -меньше расшифровывать читателю. - -## Зерно и форма - -Зерно — **день события × `utm_source` × `utm_medium` × `utm_campaign`**. -Зерно источника — событие, значит витрина материализуется: -`dm.utm_effectiveness_rep` / `_dist`, донор `_stage_rep` / `_stage_dist`, -публичное `dm.utm_effectiveness_v`. Ключ партиции — день. - -Источник — `dds.event_v`, публичный договор события: витрина слоя DM читает -договор DDS, а не соседнюю витрину. - -## Состав колонок - -| Колонка | Тип | Что это | +| Колонка | Тип | Значение | |---|---|---| -| `report_date` | `Date` | день события | -| `utm_source` | `LowCardinality(String)` | источник кампании | -| `utm_medium` | `LowCardinality(String)` | канал кампании | -| `utm_campaign` | `String` | имя кампании | -| `visitors` | `UInt64` | число разных `client_id` | -| `pageviews` | `UInt64` | просмотров страниц | -| `add_to_carts` | `UInt64` | добавлений в корзину | -| `purchases` | `UInt64` | покупок | -| `declared_revenue` | `Decimal(18, 2)` | выручка покупок глазами клиента | -| `_load_id` | `String` | запуск, собравший день | -| `_load_ts` | `DateTime64(3, 'UTC')` | когда собран | +| `report_date` | `Date` | День события | +| `utm_source` | `LowCardinality(String)` | Источник кампании | +| `utm_medium` | `LowCardinality(String)` | Канал кампании | +| `utm_campaign` | `String` | Имя кампании | +| `visitors` | `UInt64` | Число разных `client_id` | +| `pageviews` | `UInt64` | Число просмотров страниц | +| `add_to_carts` | `UInt64` | Число добавлений в корзину | +| `purchases` | `UInt64` | Число покупок | +| `declared_revenue` | `Decimal(18, 2)` | Сумма покупок, заявленная клиентом | +| `_load_id` | `String` | Идентификатор запуска, собравшего день | +| `_load_ts` | `DateTime64(3, 'UTC')` | Время сборки | -`declared_revenue` складывает `purchase_revenue[1]` событий `purchase`. -Индекс не описка: блок `purchase*` едет параллельными массивами, и у события -покупки в них ровно один элемент — контракт источника обещает один номер -заказа и одну объявленную сумму на событие. Тем же индексом берет свою -сторону стендовая сверка `purchase_vs_orders`. +Для `declared_revenue` сложите `purchase_revenue[1]` событий `purchase`. В +параллельных массивах `purchase*` по контракту хранятся один номер заказа и +одна сумма на событие. В сверке `purchase_vs_orders` используют тот же +индекс. -Приводить тип не нужно: в договоре `purchase_revenue` уже -`Array(Decimal(18, 2))`. `Float64` эта сумма была только в выгрузке -источника, и ODS перевел ее в точные деньги на приеме — сложение Decimal -точное, и где стоит `CAST`, до или после `sum`, на результат не влияет. -Витрина просто хранит то, что сложила, в `Decimal(18, 2)`. +Тип `purchase_revenue` — `Array(Decimal(18, 2))`: исходный `Float64` +преобразован в ODS. Сложение Decimal точное; `CAST` до или после `sum` не +меняет результат и не нужен. Тип итога — `Decimal(18, 2)`. -`utm_campaign` оставлена обычной строкой: имен кампаний со временем -становится много, и словарь `LowCardinality` на растущем множестве -перестает окупаться. +Для `utm_campaign` выбран тип `String`: имен кампаний со временем становится +много, и затраты на словарь `LowCardinality` перестают окупаться. -## Ритм пересборки +### Ритм пересборки -Источник — события: день замерзает сразу. Объем прогона и механика те же, -что в задании 2: хвост несобранных дней, донор, замена дневных партиций. +События прожитого дня больше не меняются. Как в задании 2, соберите в донор +дни от первого несобранного до последнего прожитого и замените дневные +партиции. Добавьте задачу в `dm_transform`. -## Ключ шардирования +### Ключ шардирования -Ваше решение по [правилу донора](../architecture/storage.md), «Раскладка по -шардам». Зерно здесь составное, из трех колонок, и решение «взять хеш от -одной из них» надо защищать отдельно: назовите, какая доля строк придется на -самое частое значение выбранной колонки. +Выберите ключ самостоятельно. Если берете хеш одной из трех UTM-колонок, +посчитайте долю строк с ее самым частым значением и обоснуйте выбор. -## Конфликт, ради которого витрина и написана +Ключ донора — тот же, что у цели (см. [«Как принято на ступени +1»](README.md#как-принято-на-ступени-1) и раздел «Раскладка по шардам» +[справочника хранилища](../architecture/storage.md)). -Соберите за один и тот же день две суммы: `sum(declared_revenue)` из своей -витрины и `sum(revenue)` из `dm.revenue_daily_v`. Они не сойдутся, и это -верное поведение обеих витрин. +## Атрибуция по событию, а не по визиту -Базы у двух сумм при этом сравнимые: `revenue_daily` складывает цену позиций -на количество и ни скидки, ни доставки в себя не берет — ровно то, что видит -клиент. Значит все расхождение до копейки объясняется тем, что случилось с -заказом, а не тем, как считали деньги. Ищите ответы среди таких причин: +Группируйте события по их собственным меткам. Модели «первое касание» и +«последнее касание» здесь не рассчитываются. -- отмененный заказ из кликстрима никуда не девается: событие покупки - случилось, а `revenue_daily` считает только оплаченные; -- событие покупки может потеряться по дороге, а заказ при этом доехать — - и наоборот, событие приехать дважды; -- клиент назвал сумму, отличную от серверной суммы позиций: генератор - делает это намеренно у одного-двух заказов из ста. +В `dds.session_v` записаны UTM первого события визита. Распределение по +каналам может отличаться от этой витрины, и это не означает ошибку в +расчете. В отчете различайте атрибуцию визитов и событий. -Как эти же расхождения выглядят на зерне заказа, показывает стендовая -витрина `dm.purchase_vs_orders_v` с ее классами расхождений: это готовый -ответ на вопрос «а сколько именно и из-за чего». Запишите свой вывод — -в двух-трех фразах, своими словами. +## Прямой трафик остается в витрине -Проверять сходимость этих двух сумм не нужно и нельзя: они и не должны -сходиться. +Сохраните события без меток: прямой трафик обычно крупнейший канал, и без +него доля рекламы будет завышена. Если отфильтровать такие строки, дневные +итоги перестанут сходиться с источником — а по этому совпадению вы +проверяете сборку. Разница составит вклад исключенных событий. + +Оставьте пустые метки пустыми строками: новое обозначение прямого трафика +потребовало бы объяснения. + +## Расхождение с `dm.revenue_daily_v` + +Сравните за один день `sum(declared_revenue)` своей витрины и `sum(revenue)` +из `dm.revenue_daily_v`. Суммы не сходятся; проверять их равенство не нужно. + +В `revenue_daily` складываются цены позиций, умноженные на количество, без +скидок и доставки — та же основа, что у клиентской суммы. Расхождение +объясняется историей заказов, а не способом расчета: + +- событие покупки остается после отмены заказа, а `revenue_daily` учитывает + только оплаченные заказы; +- событие покупки может потеряться при доставке, хотя заказ поступил, или + поступить дважды; +- клиентская сумма может отличаться от серверной суммы позиций: генератор + намеренно создает это расхождение у одного-двух заказов из ста. + +Запишите свой вывод в двух-трех фразах. Число расхождений по заказам и их +причины можно посмотреть в `dm.purchase_vs_orders_v`, где выделены классы +расхождений. ## Как проверить себя -Деловой ключ — тройка `utm_source`, `utm_medium`, `utm_campaign` внутри -дня. Соберите прямой агрегат из `dds.event_v` за день, соедините с -`dm.utm_effectiveness_v` полным внешним соединением и посчитайте строки, где -ключа нет с одной из сторон или где расходится любая из пяти величин. Ноль — -зеленый. +Деловой ключ — `utm_source`, `utm_medium`, `utm_campaign` внутри дня. +Получите прямой агрегат из `dds.event_v` за день и соедините его с +`dm.utm_effectiveness_v` полным внешним соединением по ключу. Посчитайте +строки, где ключ отсутствует с одной из сторон или различается любая из пяти +величин. Расхождений должно быть ноль. -Две проверки покрупнее ловят самые частые ошибки: +Дополнительно сверьте дневные итоги: -- сумма `pageviews`, `add_to_carts` и `purchases` за день равна числу - событий соответствующего типа в `dds.event_v` за тот же день; -- сумма `declared_revenue` за день равна сумме `purchase_revenue[1]` по всем - событиям `purchase` этого дня. +- суммы `pageviews`, `add_to_carts` и `purchases` равны числу событий + соответствующих типов в `dds.event_v`; +- сумма `declared_revenue` равна сумме `purchase_revenue[1]` по всем + событиям `purchase` того же дня. -Обе не сойдутся, если прямой трафик отфильтрован, и разойдутся ровно на его -долю — по этому следу отфильтрованные строки и находят. `visitors`, как и в -задании 3, по строкам не складывается: один посетитель приходит по разным -меткам. +Сумма `visitors` по строкам повторно учитывает одного посетителя, +приходящего по разным меткам. Для числа посетителей дня она не подходит. ## Приемка -- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется - ключом цели; -- [ ] строки без UTM-меток в витрине остались; +- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом + цели; +- [ ] сохранены строки без UTM-меток; - [ ] `declared_revenue` хранится в `Decimal(18, 2)`; - [ ] расхождение с `dm.revenue_daily_v` объяснено письменно; -- [ ] задача `utm_effectiveness` в `homework_check` зеленая; +- [ ] задача `utm_effectiveness` в `homework_check` завершилась успешно; - [ ] задача витрины добавлена в `dm_transform`.