Compare commits

..
3 Commits
Author SHA1 Message Date
ddmitry 8b9258ed41 Merge pull request 'docs(labs): проза пяти ТЗ ступени 1 и раздел «Как принято на ступени 1»' (#229) from 228-labs-prose into main
Reviewed-on: #229
2026-09-06 16:05:27 +03:00
ddadminandClaude Fable 5.1 65575346b6 docs(labs): уточнены строки ступеней 3 и 5 в программе и слово «руками»
- Зачем:
  - владелец при чтении PR #229 отметил образы в таблице программы
    («счет показывает проблему», «построить сторожей») и оборот
    «запускается рукой».
- Что:
  - ячейки «Чему учит» ступеней 3 и 5 переписаны как навыки, без образов.
  - «запускается рукой» заменено на «запускается руками».
- Проверка:
  - grep по «ё», «менти», словам-законам по docs/labs/*.md молчит.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 09:00:43 -04:00
ddadminandClaude Fable 5.1 29a01f0016 docs(labs): переписана проза пяти ТЗ ступени 1 и раздел «Как принято»
- Зачем:
  - владелец при чтении забраковал прозу ТЗ ступени 1: образы вместо
    утверждений, заголовки-метафоры, договоренности стенда поданы как
    законы. Тикет #228.
- Что:
  - пять ТЗ (задания 1–5) написаны заново по форме ТЗ ступени 2:
    содержание, колонки, формулы, самопроверка и приемка сохранены.
  - раздел README лаб «Общие правила ступени 1» переименован в «Как принято
    на ступени 1» и переписан как выбор с доводом; точечно поправлены
    «Самопроверка» и таблица ступени 1.
- Проверка:
  - grep по «ё», «менти», формам «ты» и словам-законам по docs/labs/*.md
    молчит (единственное «правило» — в нетронутом разделе «Уборка»).
  - скрипт ссылок и якорей из #226 печатает пусто; git diff --check чист.
  - сверка имен в обратных кавычках и чекбоксов старых и новых файлов —
    без расхождений.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 08:44:27 -04:00
6 changed files with 475 additions and 525 deletions

No files matched your search

+56 -61
View File
@@ -10,9 +10,9 @@
|---|---|---| |---|---|---|
| 1 | Пять витрин слоя DM | построить результат для потребителя хранилища; уроки каждой витрины — в таблице ниже | | 1 | Пять витрин слоя DM | построить результат для потребителя хранилища; уроки каждой витрины — в таблице ниже |
| 2 | [Свой график в Superset](superset-chart.md) | превратить свой результат из хранилища в отчет в BI | | 2 | [Свой график в Superset](superset-chart.md) | превратить свой результат из хранилища в отчет в BI |
| 3 | Нарежь сессии сам | счет показывает проблему, но причину приходится искать в отдельных строках | | 3 | Нарежь сессии сам | сравнить свою нарезку визитов с эталоном по числу сессий и найти причину расхождения в отдельных событиях |
| 4 | Перепиши нарезку стенда | изменить расчетную сущность внутри готового конвейера и проверить ее внешним эталоном | | 4 | Перепиши нарезку стенда | изменить расчетную сущность внутри готового конвейера и проверить ее внешним эталоном |
| 5 | Сначала сторож, потом ломай: заказы | построить сторожей до рискованного изменения, а не после первой поломки | | 5 | Сначала сторож, потом ломай: заказы | написать проверки качества до рискованного изменения, а не после первой поломки |
| 6 | Разбери сырые события и сверь результат со стендом | изучить разбор потока отдельно от живого приема и сверить его со стендовым результатом | | 6 | Разбери сырые события и сверь результат со стендом | изучить разбор потока отдельно от живого приема и сверить его со стендовым результатом |
| 7 | Мониторинг: куда смотреть | выбрать, куда и на что смотреть в живой системе | | 7 | Мониторинг: куда смотреть | выбрать, куда и на что смотреть в живой системе |
@@ -27,77 +27,73 @@
| № | Результат | Чему учит | | № | Результат | Чему учит |
|---|---|---| |---|---|---|
| 1 | [`events_enriched_v`](events-enriched.md) | слой — это контракт, а не копия данных | | 1 | [`events_enriched_v`](events-enriched.md) | строить слой как договор о данных, а не как их копию |
| 2 | [`top_pages_daily`](top-pages-daily.md) | зерно сменилось — витрина материализуется и встает в конвейер | | 2 | [`top_pages_daily`](top-pages-daily.md) | создавать материализованную витрину с дневной пересборкой в конвейере |
| 3 | [`session_overview`](session-overview.md) | зерно решает, на какие вопросы витрина уже не ответит | | 3 | [`session_overview`](session-overview.md) | хранить слагаемые для расчета средних и долей |
| 4 | [`dq_errors_daily`](dq-errors-daily.md) | у брака свой день и свой срок жизни | | 4 | [`dq_errors_daily`](dq-errors-daily.md) | считать брак по дню загрузки и хранить итоги дольше источника |
| 5 | [`utm_effectiveness`](utm-effectiveness.md) | `declared_` в имени — предупреждение, а не украшение | | 5 | [`utm_effectiveness`](utm-effectiveness.md) | читать `declared_` как предупреждение о сумме со слов клиента |
Общее устройство слоя — [дока DM](../architecture/dm.md). Она отвечает на Общее устройство слоя — [дока DM](../architecture/dm.md). Она отвечает на
вопросы «почему так», задание отвечает на вопрос «что сделать». вопросы «почему так», задание отвечает на вопрос «что сделать».
## Общие правила ступени 1 ## Как принято на ступени 1
Они одни для пяти заданий по витринам ступени 1, поэтому записаны здесь, а не **Читать — через `_v`.** Физические `_rep` и `_dist` служат загрузке и
повторяются в каждом ТЗ. обслуживанию, а публичный договор DDS — `dds.event_v` или `dds.session_v` —
остается прежним даже при изменении физических объектов. Чтение DDS
напрямую делает задания независимыми: соседняя витрина может быть еще не
построена.
Исключение — задание 4: у таблиц брака публичного договора нет.
**Читать — через `_v`.** Публичный договор сущности DDS — представление с **Писать — в `_dist`.** Так строки распределяются по ключу шардирования. При
суффиксом `_v`: `dds.event_v`, `dds.session_v`. Витрина называет только записи в локальную таблицу распределение зависит от узла, выполнившего
его; физические `_rep` и `_dist` служат загрузке и обслуживанию. Соседнюю запрос, и при следующем запуске может измениться. Подробности — раздел
домашку витрина источником тоже не берет: витрина слоя DM читает договор «Раскладка по шардам» [справочника хранилища](../architecture/storage.md).
DDS, и задания остаются независимыми. Исключение одно и оно оговорено в
задании 4: у таблиц брака публичного договора нет.
**Писать — только в `_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` с явным списком колонок. Витрина, сохраняющая зерно, Объявите донор через `AS <цель>_rep` с ключом цели: иначе замена партиции
остается обычным представлением. Довод — [дока DM](../architecture/dm.md), потеряет строку на одном шарде и удвоит на другом. Детали — раздел
«Форма: по зерну, не по цене запроса». «Раскладка по шардам» [справочника хранилища](../architecture/storage.md).
Пример обоснования — `dm.revenue_daily_dist` в
[`sql/ddl/55-dm-tables.sql`](../../sql/ddl/55-dm-tables.sql).
**Ключ шардирования — ваше решение.** Ни одно ТЗ ключа не называет. **Где лежит код.** DDL заданий — в `sql/ddl/70-labs.sql`: он применяется
Правило выбора и требование к донору — [дока после стендовых, поэтому ваши объекты не мешаются со стендовыми. В
хранилища](../architecture/storage.md), «Раскладка по шардам»: донор `sql/dm/<витрина>_scope.sql` определяются дни прогона, в `_rebuild.sql` они
объявляется через `AS <цель>_rep` и шардируется тем же ключом, что цель, собираются в донор, в `_replace.sql` заменяется партиция. Задачу сборки
иначе замена партиции потеряет строку на одном шарде и задвоит на другом. добавьте в [`dags/dm_transform.py`](../../dags/dm_transform.py): без нее
Названный ключ надо уметь защитить: почему он не дает перекоса и почему конвейер не обновит витрину. Представлению достаточно `CREATE VIEW`.
более сложный тут ничего не добавляет. Образец такой защиты —
`dm.revenue_daily_dist` в [`sql/ddl/55-dm-tables.sql`](../../sql/ddl/55-dm-tables.sql).
**Где лежит код.** DDL домашек — свой файл `sql/ddl/70-labs.sql`: он **Образец — витрина `revenue_daily`.** У нее есть пять объектов, донор,
применяется после стендовых по порядку имени, и ваши объекты не мешаются сборка, замена дневных партиций и задача в конвейере. Используйте ее как
со стендовыми. У агрегатов рядом со стендовыми ложатся еще три запроса — образец всего порядка пересборки. Состав колонок берите из своего ТЗ, иначе
`sql/dm/<витрина>_scope.sql` спрашивает у хранилища, какие дни пересобирает получите показатели другой витрины.
запуск, `_rebuild.sql` собирает их в донор, `_replace.sql` заменяет
партицию, — и задача в
[`dags/dm_transform.py`](../../dags/dm_transform.py). У разминочного
представления нет ничего из этого: весь его код — одно `CREATE VIEW`.
**Образец на все случаи** — витрина `revenue_daily`: у нее есть все пять
объектов, донор, сборка, замена дневных партиций и задача в конвейере.
Списывать с нее устройство можно и нужно; списывать состав колонок — нет,
он у каждой витрины свой.
## Самопроверка ступени 1 ## Самопроверка ступени 1
Даг `homework_check` запускается рукой, расписания у него нет. Задача Даг `homework_check` запускается руками, расписания у него нет. Задача
витрины красная, пока витрины нет или счет не сходится, и зеленая, когда витрины красная, пока витрины нет или счет не сходится, и зеленая, когда
собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет: собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет: это
это инструмент лабы, а не история качества стенда. инструмент лабы, а не история качества стенда.
Проверка пяти заданий по витринам устроена одинаково и золотых констант не Проверка пяти заданий по витринам устроена одинаково и золотых констант не
содержит: рядом с витриной независимым запросом собирается тот же ответ — содержит: рядом с витриной независимым запросом собирается тот же ответ —
прямо из источника витрины, минуя ее саму. У четырех заданий источник эталона прямо из источника витрины, минуя ее саму. У четырех заданий источник
тот же, что у витрины, `dds.session_v` или `dds.event_v`; у задания 4 это эталона тот же, что у витрины, `dds.session_v` или `dds.event_v`; у задания
таблицы брака ODS, потому что в DDS у брака представительства нет. Обе стороны 4 это таблицы брака ODS, потому что в DDS нет сущности брака. Обе стороны
соединяются по деловому ключу через `FULL OUTER JOIN`, и считается число строк, соединяются по деловому ключу через `FULL OUTER JOIN`, и считается число
где стороны разошлись или одной стороны нет. Ноль — зеленый. строк, где стороны разошлись или одной стороны нет. Ноль — зеленый. Полное
Полное соединение здесь обязательно: при обычном пропавший день или соединение сохраняет ключи обеих сторон: при внутреннем пропавший день или
пропавший ключ просто не попал бы в результат и остался бы незамечен. ключ не попал бы в результат, и потеря осталась бы незамеченной.
Раздел «Как проверить себя» каждого ТЗ по витрине называет деловой ключ и Раздел «Как проверить себя» каждого ТЗ по витрине называет деловой ключ и
сравниваемые величины. Тот же прием в стендовом исполнении — [даг сравниваемые величины. Тот же прием в стендовом исполнении — [даг
@@ -105,11 +101,10 @@ DDS, и задания остаются независимыми. Исключе
**Предупреждение.** Запросы самой проверки лежат в `sql/dq/` открыто, и **Предупреждение.** Запросы самой проверки лежат в `sql/dq/` открыто, и
имена называют витрину и роль сверки. Проверка на инвариантах неизбежно имена называют витрину и роль сверки. Проверка на инвариантах неизбежно
близка к ответу: чтобы сверить витрину, надо ее пересчитать. Прятать эти близка к ответу: чтобы сверить витрину, надо ее пересчитать. Если открыть
файлы никто не станет — но открыв их до того, как задание сделано, вы эти файлы до выполнения задания, можно скопировать готовый SQL, не
обменяете урок на готовый SQL. разобравшись в расчете самостоятельно. Смотреть туда стоит после своей
Смотреть туда стоит после своей сборки: сравнить два решения полезно, сборки: сравнить два решения полезно, списать одно — нет.
списать одно — нет.
## Факультативы ## Факультативы
+83 -88
View File
@@ -1,118 +1,113 @@
# Задание 4: `dm.dq_errors_daily` # Задание 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` | | `source` | `LowCardinality(String)` | `event` или `order` |
| `error_class` | `LowCardinality(String)` | класс брака из таблицы ошибок | | `error_class` | `LowCardinality(String)` | Класс брака из таблицы ошибок |
| `error_rows` | `UInt64` | число забракованных строк | | `error_rows` | `UInt64` | Число забракованных строк |
| `_load_id` | `String` | запуск, собравший день | | `_load_id` | `String` | Идентификатор запуска, собравшего день |
| `_load_ts` | `DateTime64(3, 'UTC')` | когда собран | | `_load_ts` | `DateTime64(3, 'UTC')` | Время сборки |
Две похожие колонки рядом различаются намеренно: `load_date` — про день, Вычислите `load_date` как `toDate(_load_ts)` из таблицы ошибок — это
когда приехал брак, `_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`. Область Деловой ключ — `source` и `error_class` внутри `load_date`. Проверяйте дни,
проверки — дни загрузки, которые еще живы хотя бы в одной таблице ошибок: оставшиеся хотя бы в одной таблице ошибок. Если дня за пределами `TTL` уже
день за границей `TTL` сверять не с чем, и его отсутствие в источнике не нет в источнике, это не ошибка: сверять его уже не с чем.
ошибка.
Соберите прямой счет по обеим таблицам ошибок, соедините с Посчитайте строки прямо в обеих таблицах ошибок и соедините результат с
`dm.dq_errors_daily_v` полным внешним соединением по ключу и посчитайте `dm.dq_errors_daily_v` полным внешним соединением по ключу. Затем посчитайте
строки, где ключа нет с одной из сторон или где расходится `error_rows`. строки, где ключ отсутствует с одной из сторон или различается `error_rows`.
Ноль — зеленый. Расхождений должно быть ноль.
Отдельно: сумма `error_rows` по живым дням равна общему числу строк в Сумма `error_rows` по проверяемым дням равна общему числу строк в
`ods.event_errors_dist` плюс `ods.order_errors_dist`. Если эта сумма вышла `ods.event_errors_dist` и `ods.order_errors_dist`. Если она примерно вдвое
примерно вдвое меньше — витрина почти наверняка собрана с `_rep` вместо меньше, вероятная причина — чтение `_rep` вместо `_dist`.
`_dist` и видит один шард из двух.
**Пустая витрина — верный ответ.** На чистом стенде брака нет, обе стороны Пустая витрина на чистом стенде — верный ответ: обе стороны пусты,
пусты, расхождений ноль, задача зеленая. Проверка, которая требует хотя бы расхождений ноль, проверка успешна. Требование хотя бы одной строки дало бы
одной строки, на здоровом стенде краснела бы всегда и приучала бы себя постоянную ошибку, которую со временем стали бы игнорировать.
игнорировать.
## Приемка ## Приемка
- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется - [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом
ключом цели; цели;
- [ ] источники — `_dist`-таблицы обоих видов брака; - [ ] источники — таблицы `_dist` обоих видов брака;
- [ ] прогон заменяет партиции только живых дней источника и не трогает - [ ] прогон заменяет партиции только дней, еще имеющихся в источниках, и
истекшие; сохраняет дни за пределами их срока хранения;
- [ ] задача `dq_errors_daily` в `homework_check` зеленая — в том числе на - [ ] задача `dq_errors_daily` в `homework_check` завершилась успешно, в том
чистом стенде, где брака нет; числе на чистом стенде без брака;
- [ ] задача витрины добавлена в `dm_transform`. - [ ] задача витрины добавлена в `dm_transform`.
+63 -84
View File
@@ -1,114 +1,93 @@
# Задание 1: `dm.events_enriched_v` # Задание 1: `dm.events_enriched_v`
**Чему учит:** слой — это контракт, а не копия данных. Витрина, которая не **Чему учит:** строить слой как договор о данных, а не как их копию.
меняет зерно источника, остается представлением, и копии событий ради Оставлять витрину представлением, если зерно источника не меняется.
других имен колонок не заводит ни один боевой склад.
Это разминка. Строк она не хранит, в конвейер не встает, ключа шардирования ## Сначала прочитать
у нее нет. Задача одна: собрать плоское событие для BI и не поддаться
соблазну разложить его на диск. - [«Хранилище и его слои»](../handbook/storage-layers.md).
## Роль витрины ## Роль витрины
Договор события в DDS широкий — 44 колонки, почти весь формат выгрузки: В `dds.event_v` 44 колонки, включая технические поля источника и массивы
часть из них технические координаты источника, часть — параллельные массивы товаров. Для анализа событий в Superset нужны деловые поля и три готовых
товаров. Аналитику в Superset нужен плоский набор деловых полей и пара значения: час события, путь страницы и домен перехода. Тогда вам не придется
производных, которые иначе каждый пишет заново в каждом запросе. повторять эти выражения в каждом запросе.
Витрина отвечает на вопрос «что вообще происходило»: с нее начинают, когда Витрины следующих заданий читают DDS напрямую. Использовать
надо посмотреть на события глазами человека, а не разбирать широкую строку `dm.events_enriched_v` как источник для них не нужно: задания независимы, их
источника. можно выполнять в любом порядке.
Источником для витрин следующих заданий она не служит. Витрина слоя DM ## Что сделать
читает публичный договор DDS — так устроены и стендовые витрины, и потому
задания независимы: соседнюю домашку можно делать первой, а можно не делать
вовсе.
## Зерно и форма Создайте обычное представление `CREATE VIEW dm.events_enriched_v`.
Единственный источник — `dds.event_v`. Зерно — **событие**: каждой строке
источника соответствует одна строка витрины.
Зерно — **событие**: одна строка витрины равна одной строке `dds.event_v`. Физическая таблица, таблица-донор и задача в `dm_transform` не нужны.
Зерно источника сохранено, значит форма — обычное представление Представление не хранит строки, поэтому ключ шардирования и пересборка ему
`CREATE VIEW dm.events_enriched_v`, без физической таблицы, без донора и не требуются.
без задачи в `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)` | суточный профиль спрашивают в каждом отчете | | `event_hour` | `toHour(event_time)` | Анализ событий по часам суток |
| `page_path` | путь `url` без домена и параметров | группировка по странице без разбора строки в каждом запросе | | `page_path` | путь `url` без домена и параметров | Группировка по странице |
| `referer_host` | домен `referer` | «откуда пришли» одним значением, а не полным адресом | | `referer_host` | домен `referer` | Анализ источников перехода |
Служебных `_load_id` и `_load_ts` у витрины нет, и это не забывчивость. Их Не добавляйте `_load_id` и `_load_ts`. Их нет и у `dds.event_v`: события в
нет и у `dds.event_v`: событие в DDS не грузят, а читают из ODS, поэтому DDS читают из ODS без отдельной загрузки. Эти служебные поля указывают на
координат собственной загрузки у него не бывает. Служебная колонка называет запуск, который записал строку на диск. Представление ничего не загружает.
запуск, который положил строку на диск, — здесь на диск никто ничего не
кладет.
## Ловушка задания ## Типичная ошибка
Массивы `product_*` тянет развернуть через `ARRAY JOIN` — так плоская Не разворачивайте массивы товаров через `ARRAY JOIN`. У события с тремя
таблица становится по-настоящему плоской. Разворачивать нельзя, и вреда позициями получится три строки. Зерно изменится. Витрину с новым зерном на
здесь два. стенде хранят готовым агрегатом — причины описаны в разделе [«Как принято на
ступени 1»](README.md#как-принято-на-ступени-1).
Очевидный: у события с тремя позициями получилось бы три строки, зерно Главная опасность — потеря событий с пустым массивом товаров: `ARRAY JOIN`
витрины перестало бы совпадать с зерном источника, а вместе с ним развалилось исключает их из результата. Товары есть только в событиях корзины и покупки.
бы и правило формы — такую витрину уже пришлось бы материализовать. Поэтому исчезнет большинство событий — девятнадцать строк из двадцати.
Запрос выполнится без ошибки, а витрина по имени и составу колонок
по-прежнему будет выглядеть таблицей событий.
Неочевидный и куда более разрушительный: `ARRAY JOIN` — соединение Сохраните массивы как в источнике. Для анализа отдельных товарных позиций
внутреннее, и строку с пустым массивом он выбрасывает. Торговый блок пуст у нужна другая витрина с другим зерном и именем.
всех событий, кроме корзин и покупок, то есть у подавляющего большинства.
Так «плоская таблица событий» превращается в таблицу товарных позиций и
теряет девятнадцать строк из двадцати — а по имени и по составу колонок она
по-прежнему выглядит таблицей событий. Ошибка не падает и ничего не
подсвечивает.
Массивы едут в договор как есть; разворачивает их тот, кому нужна позиция
заказа, — и делает это уже в своей витрине, с другим зерном и другим именем.
## Как проверить себя ## Как проверить себя
Деловой ключ — `event_id` внутри дня. Соберите прямой ответ из Деловой ключ — `event_id` внутри дня. Получите результат прямо из
`dds.event_v` за тот же день, соедините с витриной полным внешним `dds.event_v` за тот же день. Соедините его с витриной полным внешним
соединением и посчитайте строки, где ключа нет с одной из сторон или где соединением по `event_id`. Посчитайте строки, где ключ отсутствует с одной
различаются значения деловых колонок. Ноль — зеленый. из сторон или различаются значения деловых колонок. Расхождений должно быть
ноль.
Отдельно проверяются три производные колонки: `event_hour`, `page_path` и Отдельно сверьте `event_hour`, `page_path` и `referer_host` с теми же
`referer_host` должны совпасть с тем же выражением, посчитанным по выражениями, рассчитанными прямо по `dds.event_v`.
`dds.event_v` на лету.
Если строк в витрине оказалось в разы меньше, чем в источнике, а остались в Если строк в витрине в разы меньше, чем в источнике, и остались только
ней одни корзины и покупки — это развернутые массивы: `ARRAY JOIN` выбросил события корзины и покупки, это признак развернутых массивов: `ARRAY JOIN`
все события с пустым торговым блоком. исключил все события с пустым массивом товаров.
## Приемка ## Приемка
- [ ] `dm.events_enriched_v` создано, физических таблиц под ним нет; - [ ] представление `dm.events_enriched_v` создано без физических таблиц;
- [ ] список колонок в представлении явный; - [ ] список колонок в представлении задан явно;
- [ ] число строк и набор `event_id` совпадают с `dds.event_v` за каждый - [ ] число строк и набор `event_id` совпадают с `dds.event_v` за каждый
прожитый день; прожитый день;
- [ ] задача `events_enriched` в `homework_check` зеленая; - [ ] задача `events_enriched` в `homework_check` завершилась успешно;
- [ ] задачи в `dm_transform` **нет** — и не должно быть. - [ ] задачи для этой витрины в `dm_transform` нет.
+90 -93
View File
@@ -1,121 +1,118 @@
# Задание 3: `dm.session_overview` # Задание 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` | день сессии | | `report_date` | `Date` | День сессии |
| `device_category` | `LowCardinality(String)` | категория устройства | | `device_category` | `LowCardinality(String)` | Категория устройства |
| `sessions` | `UInt64` | число визитов | | `sessions` | `UInt64` | Число визитов |
| `visitors` | `UInt64` | число разных `client_id` | | `visitors` | `UInt64` | Число разных `client_id` |
| `bounce_sessions` | `UInt64` | визиты ровно из одного события | | `bounce_sessions` | `UInt64` | Визиты ровно из одного события |
| `purchase_sessions` | `UInt64` | визиты, в которых была хотя бы одна покупка | | `purchase_sessions` | `UInt64` | Визиты хотя бы с одной покупкой |
| `duration_seconds_total` | `UInt64` | суммарная длительность визитов | | `duration_seconds_total` | `UInt64` | Суммарная длительность визитов |
| `events_total` | `UInt64` | событий во всех визитах | | `events_total` | `UInt64` | Число событий во всех визитах |
| `pageviews` | `UInt64` | просмотров страниц | | `pageviews` | `UInt64` | Число просмотров страниц |
| `cart_adds` | `UInt64` | добавлений в корзину | | `cart_adds` | `UInt64` | Число добавлений в корзину |
| `purchases` | `UInt64` | покупок | | `purchases` | `UInt64` | Число покупок |
| `_load_id` | `String` | запуск, собравший день | | `_load_id` | `String` | Идентификатор запуска, собравшего день |
| `_load_ts` | `DateTime64(3, 'UTC')` | когда собран | | `_load_ts` | `DateTime64(3, 'UTC')` | Время сборки |
Средней длительности и доли отказов в витрине нет намеренно. Хранить Возьмите `device_category` из сессии: DDS уже заменил код словом. Из четырех
`avg_duration_seconds` было бы удобнее ровно один раз — пока смотрят одну категорий встречаются `desktop`, `mobile` и `tablet`. Строку для `tv`
строку. Тот, кто захочет среднее по дню целиком, сложит средние по трем создавайте только при появлении таких визитов.
устройствам и получит неправду: средние не складываются, они взвешиваются.
Пара «`duration_seconds_total` и `sessions`» дает верный ответ на любом
срезе, а деление стоит потребителю один вызов. То же и с долей отказов:
храним `bounce_sessions` и `sessions`, долю считает тот, кто спрашивает.
С `visitors` история тоньше, и в ней главная ловушка задания. Складывать Вместо `avg_duration_seconds` храните слагаемые. Среднее из средних по трем
уникальных по строкам нельзя: один и тот же посетитель, попавший в две устройствам даст неверный ответ за день: число сессий в категориях
строки, будет посчитан дважды. Проверьте на стенде — и увидите, что сумма различается, поэтому средние нужно взвешивать. Потребитель делит сумму
`visitors` по устройствам за день **сходится** с `dm.daily_traffic_v`. `duration_seconds_total` на сумму `sessions`. Долю отказов считают так же:
сумму `bounce_sessions` делят на сумму `sessions`, а не усредняют готовые
доли.
Это не отменяет правила, а показывает, откуда берется совпадение: в мире ### Ритм пересборки
стенда устройство приписано куке на всю жизнь ([словарь
понятий](../../CONTEXT.md), «Паспорт куки»), поэтому одна кука лежит ровно
в одной строке дня, и пересекаться строкам не в чем.
Совпадение это — свойство мира, а не свойство витрины. Появятся визиты Сессии прожитого дня больше не меняются. Как в задании 2, определяйте
одной куки с двух устройств — сумма поедет, и ни одна проверка об этом не диапазон от первого несобранного прожитого дня до последнего прожитого.
скажет: отчет просто начнет тихо завышать аудиторию. Отсюда правило, Соберите его в донор и замените дневные партиции. Добавьте задачу в
которое стоит унести с задания: складывать уникальных нельзя даже там, где `dm_transform`.
сегодня получается. Число посетителей дня спрашивают у
`dm.daily_traffic_v`, где оно посчитано на своем зерне.
## Ритм пересборки ### Ключ шардирования
Источник — сессии: день замерзает, как только прожит. Объем прогона тот же, Выберите ключ самостоятельно. Визитов с планшета мало, с телефонов и
что в задании 2, — хвост от первого несобранного прожитого дня; механика та компьютеров — значительно больше. Если хотите взять хеш только от
же: донор и замена дневных партиций. `device_category`, сначала посчитайте строки на каждом шарде. Затем
объясните, почему такое распределение вас устраивает.
## Ключ шардирования Ключ донора — тот же, что у цели (см. [«Как принято на ступени
1»](README.md#как-принято-на-ступени-1) и раздел «Раскладка по шардам»
[справочника хранилища](../architecture/storage.md)).
Ваше решение по [правилу донора](../architecture/storage.md), «Раскладка по ## Почему сумма `visitors` сходится с `dm.daily_traffic_v`
шардам». Здесь у измерения всего три встречающихся значения, и распределены
они неровно: планшеты — тонкая полоса, а телефоны и настольные машины держат Сравните сумму `visitors` по устройствам за день с `dm.daily_traffic_v`. На
основную массу. Прежде чем брать хеш от одного `device_category`, посчитайте, стенде значения совпадут: устройство приписано куке на всю жизнь, поэтому за
сколько строк ляжет на каждый шард, и назовите, чем вас устраивает результат. день она учитывается только в одной строке. Это свойство мира стенда; см.
раздел «Паспорт куки» в [словаре понятий](../../CONTEXT.md).
Если появятся визиты одной куки с двух устройств, сумма начнет завышать
аудиторию. Проверки счетчиков по категориям этого не обнаружат. Число
посетителей дня берите из `dm.daily_traffic_v`, где оно рассчитано для дня
целиком.
## Типичная ошибка
Повторное выделение визитов из событий может дать другие границы сессий, чем
в DDS. Используйте готовые сессии из `dds.session_v`. Если общее число
событий совпало с источником, а число сессий завышено, вероятная причина —
сборка витрины из событий вместо сессий.
## Как проверить себя ## Как проверить себя
Деловой ключ — `device_category` внутри дня. Соберите прямой агрегат из Деловой ключ — `device_category` внутри дня. Получите прямой агрегат из
`dds.session_v` за день, соедините с `dm.session_overview_v` полным внешним `dds.session_v` за день. Соедините его с `dm.session_overview_v` полным
соединением и посчитайте строки, где ключа нет с одной из сторон или где внешним соединением по ключу. Посчитайте строки, где ключ отсутствует с
расходится любой счетчик. Ноль — зеленый. одной из сторон или различается любой счетчик. Расхождений должно быть ноль.
Две проверки покрупнее ловят грубую ошибку сразу: Еще две проверки дневных итогов:
- сумма `sessions` за день равна числу строк `dds.session_v` этого дня: - сумма `sessions` равна числу строк `dds.session_v` за день — без потерь и
ни один визит не потерян и ни один не задвоен; повторного учета визитов;
- сумма `events_total` за день равна сумме `events_total` по сессиям того - сумма `events_total` равна сумме `events_total` по сессиям того же дня.
же дня.
Если суммы событий сходятся, а сумма сессий больше числа визитов — почти
наверняка витрина собрана из событий, а не из сессий.
## Приемка ## Приемка
- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется - [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом
ключом цели; цели;
- [ ] `dm.session_overview_v` перечисляет колонки явно; - [ ] в `dm.session_overview_v` колонки перечислены явно;
- [ ] источник витрины — `dds.session_v`, своей нарезки визитов в ней нет; - [ ] источник — `dds.session_v`, повторного выделения визитов нет;
- [ ] средних и долей в физической таблице нет; - [ ] физическая таблица не хранит средние и доли;
- [ ] задача `session_overview` в `homework_check` зеленая; - [ ] задача `session_overview` в `homework_check` завершилась успешно;
- [ ] задача витрины добавлена в `dm_transform`. - [ ] задача витрины добавлена в `dm_transform`.
+82 -86
View File
@@ -1,114 +1,110 @@
# Задание 2: `dm.top_pages_daily` # Задание 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`, [«Как принято на ступени 1»](README.md#как-принято-на-ступени-1). Создайте
публичное `dm.top_pages_daily_v`. пять объектов:
`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` | день события | | `report_date` | `Date` | День события |
| `page_path` | `LowCardinality(String)` | путь страницы | | `page_path` | `LowCardinality(String)` | Путь страницы |
| `pageviews` | `UInt64` | число событий `pageview` на странице | | `pageviews` | `UInt64` | Число событий `pageview` на странице |
| `visitors` | `UInt64` | число разных `client_id` на странице | | `visitors` | `UInt64` | Число разных `client_id` на странице |
| `cart_adds` | `UInt64` | число событий `add_to_cart` на странице | | `cart_adds` | `UInt64` | Число событий `add_to_cart` на странице |
| `purchases` | `UInt64` | число событий `purchase` на странице | | `purchases` | `UInt64` | Число событий `purchase` на странице |
| `_load_id` | `String` | запуск, собравший день | | `_load_id` | `String` | Идентификатор запуска, собравшего день |
| `_load_ts` | `DateTime64(3, 'UTC')` | когда собран | | `_load_ts` | `DateTime64(3, 'UTC')` | Время сборки |
Счетчики условные: строку дают все три типа событий, а различает их Вычислите `page_path` из `url`, убрав домен и параметры запроса. Полный
`countIf`. Отфильтровать источник по `event_type = 'pageview'`, как адрес разделил бы показатели одной страницы на сотни строк. Учитывайте все
подсказывает имя витрины, — значит получить витрину из одних просмотров: три типа событий, а их счетчики вычисляйте через `countIf`.
страницы останутся на месте, а `cart_adds` и `purchases` у всех строк
обнулятся. Ошибка тихая: витрина выглядит собранной, и главный ее вопрос —
конверсия карточки — молча отвечает «ноль везде».
## Ритм пересборки ### Ритм пересборки
Источник — события: день замерзает сразу, как только прожит. Значит События прожитого дня больше не меняются. Определяйте объем прогона по
пересобирать нужно только то, что еще не собрано: хвост дней, которых в состоянию витрины: от первого несобранного прожитого дня до последнего
витрине нет, — от первого несобранного прожитого дня до последнего прожитого. Отсчет от последнего собранного дня оставит пропуски после
прожитого. Готовые дни трогать незачем, а вот дыру, оставленную частичным частичного сбоя.
отказом, объем прогона обязан подобрать: ищите не последний собранный день, а
первый несобранный.
Механика — сборка отрезка в донор одной вставкой и замена дневных партиций Как у `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` внутри дня. Соберите прямой агрегат из Деловой ключ — `page_path` внутри дня. Получите прямой агрегат из
`dds.event_v` за день, соедините с `dm.top_pages_daily_v` полным внешним `dds.event_v` за день и соедините его с `dm.top_pages_daily_v` полным
соединением по ключу и посчитайте строки, где ключа нет с одной из сторон или внешним соединением по ключу. Посчитайте строки, где ключ отсутствует с
где расходится любой из четырех счетчиков. Ноль — зеленый. одной из сторон или различается любой из четырех счетчиков. Расхождений
должно быть ноль.
Две проверки попроще, ими удобно ловить грубую ошибку до полного Перед полным сравнением проверьте дневные суммы:
соединения:
- сумма `pageviews` за день равна числу событий `pageview` этого дня в - сумма `pageviews` равна числу событий `pageview` в `dds.event_v`;
`dds.event_v`; - сумма `purchases` равна числу событий `purchase` в том же источнике.
- сумма `purchases` за день равна числу событий `purchase` этого дня.
Проверки ловят разное. Фильтр `WHERE event_type = 'pageview'` первая из них Сумма `visitors` по страницам повторно учитывает посетителя, открывшего
пропускает: отбор строк по типу не меняет счет событий того же типа. несколько страниц. Число посетителей за день берите из `dm.daily_traffic_v`.
Расхождение покажет только вторая — вместе с `cart_adds` обнулятся и
`purchases`. Проверку выбирают по тому, умеет ли она покраснеть на вашей
ошибке, а не по тому, как быстро она пишется: [проверка, которая не умеет
краснеть, бесполезна](../architecture/testing.md).
`visitors` складывать по строкам нельзя: один посетитель ходит по разным
страницам и в сумме посчитается несколько раз. Число посетителей дня
спрашивают у `dm.daily_traffic_v`, а не у этой витрины.
## Приемка ## Приемка
- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется - [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом
ключом цели; цели;
- [ ] `dm.top_pages_daily_v` перечисляет колонки явно, включая `_load_id` и - [ ] в `dm.top_pages_daily_v` явно перечислены колонки, включая `_load_id`
`_load_ts`; и `_load_ts`;
- [ ] объем прогона выводится из состояния витрины, а не задается руками; - [ ] объем прогона определяется по состоянию витрины без ручного задания;
- [ ] задача `top_pages_daily` в `homework_check` зеленая; - [ ] задача `top_pages_daily` в `homework_check` завершилась успешно;
- [ ] задача витрины добавлена в `dm_transform`, повторный прогон конвейера - [ ] задача витрины добавлена в `dm_transform`, повторный прогон конвейера
результат не меняет. не меняет результат.
+101 -113
View File
@@ -1,150 +1,138 @@
# Задание 5: `dm.utm_effectiveness` # Задание 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` | день события | | `report_date` | `Date` | День события |
| `utm_source` | `LowCardinality(String)` | источник кампании | | `utm_source` | `LowCardinality(String)` | Источник кампании |
| `utm_medium` | `LowCardinality(String)` | канал кампании | | `utm_medium` | `LowCardinality(String)` | Канал кампании |
| `utm_campaign` | `String` | имя кампании | | `utm_campaign` | `String` | Имя кампании |
| `visitors` | `UInt64` | число разных `client_id` | | `visitors` | `UInt64` | Число разных `client_id` |
| `pageviews` | `UInt64` | просмотров страниц | | `pageviews` | `UInt64` | Число просмотров страниц |
| `add_to_carts` | `UInt64` | добавлений в корзину | | `add_to_carts` | `UInt64` | Число добавлений в корзину |
| `purchases` | `UInt64` | покупок | | `purchases` | `UInt64` | Число покупок |
| `declared_revenue` | `Decimal(18, 2)` | выручка покупок глазами клиента | | `declared_revenue` | `Decimal(18, 2)` | Сумма покупок, заявленная клиентом |
| `_load_id` | `String` | запуск, собравший день | | `_load_id` | `String` | Идентификатор запуска, собравшего день |
| `_load_ts` | `DateTime64(3, 'UTC')` | когда собран | | `_load_ts` | `DateTime64(3, 'UTC')` | Время сборки |
`declared_revenue` складывает `purchase_revenue[1]` событий `purchase`. Для `declared_revenue` сложите `purchase_revenue[1]` событий `purchase`. В
Индекс не описка: блок `purchase*` едет параллельными массивами, и у события параллельных массивах `purchase*` по контракту хранятся один номер заказа и
покупки в них ровно один элемент — контракт источника обещает один номер одна сумма на событие. В сверке `purchase_vs_orders` используют тот же
заказа и одну объявленную сумму на событие. Тем же индексом берет свою индекс.
сторону стендовая сверка `purchase_vs_orders`.
Приводить тип не нужно: в договоре `purchase_revenue` уже Тип `purchase_revenue` — `Array(Decimal(18, 2))`: исходный `Float64`
`Array(Decimal(18, 2))`. `Float64` эта сумма была только в выгрузке преобразован в ODS. Сложение Decimal точное; `CAST` до или после `sum` не
источника, и ODS перевел ее в точные деньги на приеме — сложение Decimal меняет результат и не нужен. Тип итога — `Decimal(18, 2)`.
точное, и где стоит `CAST`, до или после `sum`, на результат не влияет.
Витрина просто хранит то, что сложила, в `Decimal(18, 2)`.
`utm_campaign` оставлена обычной строкой: имен кампаний со временем Для `utm_campaign` выбран тип `String`: имен кампаний со временем становится
становится много, и словарь `LowCardinality` на растущем множестве много, и затраты на словарь `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` складывает цену позиций Группируйте события по их собственным меткам. Модели «первое касание» и
на количество и ни скидки, ни доставки в себя не берет — ровно то, что видит «последнее касание» здесь не рассчитываются.
клиент. Значит все расхождение до копейки объясняется тем, что случилось с
заказом, а не тем, как считали деньги. Ищите ответы среди таких причин:
- отмененный заказ из кликстрима никуда не девается: событие покупки В `dds.session_v` записаны UTM первого события визита. Распределение по
случилось, а `revenue_daily` считает только оплаченные; каналам может отличаться от этой витрины, и это не означает ошибку в
- событие покупки может потеряться по дороге, а заказ при этом доехать — расчете. В отчете различайте атрибуцию визитов и событий.
и наоборот, событие приехать дважды;
- клиент назвал сумму, отличную от серверной суммы позиций: генератор
делает это намеренно у одного-двух заказов из ста.
Как эти же расхождения выглядят на зерне заказа, показывает стендовая ## Прямой трафик остается в витрине
витрина `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` внутри Деловой ключ — `utm_source`, `utm_medium`, `utm_campaign` внутри дня.
дня. Соберите прямой агрегат из `dds.event_v` за день, соедините с Получите прямой агрегат из `dds.event_v` за день и соедините его с
`dm.utm_effectiveness_v` полным внешним соединением и посчитайте строки, где `dm.utm_effectiveness_v` полным внешним соединением по ключу. Посчитайте
ключа нет с одной из сторон или где расходится любая из пяти величин. Ноль — строки, где ключ отсутствует с одной из сторон или различается любая из пяти
зеленый. величин. Расхождений должно быть ноль.
Две проверки покрупнее ловят самые частые ошибки: Дополнительно сверьте дневные итоги:
- сумма `pageviews`, `add_to_carts` и `purchases` за день равна числу - суммы `pageviews`, `add_to_carts` и `purchases` равны числу событий
событий соответствующего типа в `dds.event_v` за тот же день; соответствующих типов в `dds.event_v`;
- сумма `declared_revenue` за день равна сумме `purchase_revenue[1]` по всем - сумма `declared_revenue` равна сумме `purchase_revenue[1]` по всем
событиям `purchase` этого дня. событиям `purchase` того же дня.
Обе не сойдутся, если прямой трафик отфильтрован, и разойдутся ровно на его Сумма `visitors` по строкам повторно учитывает одного посетителя,
долю — по этому следу отфильтрованные строки и находят. `visitors`, как и в приходящего по разным меткам. Для числа посетителей дня она не подходит.
задании 3, по строкам не складывается: один посетитель приходит по разным
меткам.
## Приемка ## Приемка
- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется - [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом
ключом цели; цели;
- [ ] строки без UTM-меток в витрине остались; - [ ] сохранены строки без UTM-меток;
- [ ] `declared_revenue` хранится в `Decimal(18, 2)`; - [ ] `declared_revenue` хранится в `Decimal(18, 2)`;
- [ ] расхождение с `dm.revenue_daily_v` объяснено письменно; - [ ] расхождение с `dm.revenue_daily_v` объяснено письменно;
- [ ] задача `utm_effectiveness` в `homework_check` зеленая; - [ ] задача `utm_effectiveness` в `homework_check` завершилась успешно;
- [ ] задача витрины добавлена в `dm_transform`. - [ ] задача витрины добавлена в `dm_transform`.