Merge pull request 'docs(labs): проза пяти ТЗ ступени 1 и раздел «Как принято на ступени 1»' (#229) from 228-labs-prose into main

Reviewed-on: #229
This commit was merged in pull request #229.
This commit is contained in:
ddmitry committed 2026-09-06 16:05:27 +03:00
commit 8b9258ed41
6 files changed
+475 -525

No files matched your search

+56 -61
View File
@@ -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, не
разобравшись в расчете самостоятельно. Смотреть туда стоит после своей
сборки: сравнить два решения полезно, списать одно — нет.
## Факультативы
+83 -88
View File
@@ -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`.
+63 -84
View File
@@ -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` нет.
+90 -93
View File
@@ -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`.
+82 -86
View File
@@ -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`, повторный прогон конвейера
результат не меняет.
не меняет результат.
+101 -113
View File
@@ -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`.