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:
commit
8b9258ed41
6 files changed
+475
-525
No files matched your search
+56
-61
@@ -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, не
|
||||
разобравшись в расчете самостоятельно. Смотреть туда стоит после своей
|
||||
сборки: сравнить два решения полезно, списать одно — нет.
|
||||
|
||||
## Факультативы
|
||||
|
||||
|
||||
@@ -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`.
|
||||
@@ -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` нет.
|
||||
@@ -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`.
|
||||
@@ -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
@@ -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`.
|
||||
Reference in new issue
Block a user