- Зачем: - учащийся меняет расчет сессии внутри готового конвейера и проверяет результат внешним эталоном. - Что: - добавлено ТЗ ступени 4 с договором трех SQL-файлов, адресным прогоном и самопроверкой. - индекс лаб ведет на задание; будущими остаются ступени 5–7. - Проверка: - make lint, относительные ссылки и git diff --check прошли. - запрос размещения сессий по шардам проверен на стенде под analyst; данные не изменялись. - выполнены независимое ревью, writing-for-agents, ai-text-lint и литературная вычитка.
169 lines
10 KiB
Markdown
169 lines
10 KiB
Markdown
# Перепиши нарезку стенда
|
||
|
||
**Чему учит:** изменить расчетную сущность внутри готового конвейера и
|
||
проверить результат внешним эталоном.
|
||
|
||
## Сначала прочитать
|
||
|
||
- [задание «Нарежь сессии сам»](3-1-sessions-by-hand.md);
|
||
- [модель сессии DDS](../architecture/dds/session.md);
|
||
- [дневной конвейер трансформаций](../architecture/etl.md#объём-прогона);
|
||
- [раскладка по шардам](../architecture/storage.md#раскладка-по-шардам).
|
||
|
||
## Граница задания
|
||
|
||
Стенд уже умеет запускать трансформацию, хранить сессии и проверять их число.
|
||
Перепишите только три файла:
|
||
|
||
- `sql/dds/session_scope.sql`;
|
||
- `sql/dds/session_rebuild.sql`;
|
||
- `sql/dds/session_replace.sql`.
|
||
|
||
Удалите из них готовый SQL и соберите свою реализацию. Даг `dds_transform`
|
||
передаст ей параметры и запустит запросы в нужном порядке. DDL уже создает
|
||
целевые таблицы и донор. Донор — служебная таблица, где день собирается перед
|
||
заменой. Запрос `sessions_vs_reference.sql` проверит опубликованный результат.
|
||
|
||
## Договор с дагом
|
||
|
||
Объем прогона — дни, которые соберет один запуск. Его вычисляет первый файл.
|
||
|
||
| Файл | Вход | Результат |
|
||
|---|---|---|
|
||
| `session_scope.sql` | `{position:UInt16}` — номер первого несыгранного дня | `first_day` и `last_day` |
|
||
| `session_rebuild.sql` | `{first_day:Date}`, `{last_day:Date}`, `{load_id:String}` | Собранные сессии в `dds.session_stage_dist` |
|
||
| `session_replace.sql` | День в `{{ params.day }}` | Партиция дня в `dds.session_rep` |
|
||
|
||
`session_scope.sql` ищет первый прожитый день, которого еще нет в DDS. От него
|
||
до последнего прожитого дня получается непрерывный отрезок. Начало шкалы нужно
|
||
взять из событий ODS: `position` считает дни от него. Живой модельный день еще
|
||
не закончен, поэтому сессии за него не собираются.
|
||
|
||
Если в ODS нет ни одного события, запрос не возвращает строк. Если DDS уже
|
||
догнал мир, запрос возвращает строку с `first_day = NULL`. Даг поймет, что
|
||
работы нет, и пропустит группу сессий.
|
||
|
||
`session_rebuild.sql` сначала очищает `dds.session_stage_rep` на всем кластере.
|
||
Затем он собирает в `dds.session_stage_dist` все дни отрезка. Вставка должна
|
||
закончиться до замены партиций.
|
||
|
||
`session_replace.sql` заменяет один день. Цель — локальная таблица
|
||
`dds.session_rep`, источник — локальный донор `dds.session_stage_rep`. Операция
|
||
выполняется на всем кластере.
|
||
|
||
`session_scope.sql` читается как один запрос, поэтому в конце ему не нужна
|
||
точка с запятой. День для замены подставляет шаблон Jinja в Airflow. Параметр
|
||
ClickHouse в `ALTER ... PARTITION` здесь не работает.
|
||
|
||
## Нарежьте сессии
|
||
|
||
Источник сборки — `dds.event_v`. Одна строка результата представляет один
|
||
визит. Используйте правила, которые уточнили на третьей ступени:
|
||
|
||
- сессия не пересекает модельные сутки;
|
||
- пауза больше 30 минут открывает новую сессию;
|
||
- в нарезке участвуют события всех типов;
|
||
- при одинаковом времени порядок событий задает `event_id`, поэтому полный
|
||
порядок окон — `(event_time, event_id)`;
|
||
- `session_id` детерминирован и зависит от куки и начала сессии.
|
||
|
||
Состав и типы строки заданы в `dds.session_rep`. Заполните начало, конец и
|
||
длительность. Посчитайте все события, просмотры, добавления в корзину и
|
||
покупки. Возьмите входную и выходную страницы, UTM первого события, устройство
|
||
и город. Служебные колонки тоже входят в договор таблицы.
|
||
|
||
Пишите через `dds.session_stage_dist`. Эта таблица разложит строки по ключу
|
||
донора.
|
||
|
||
## Запустите свою реализацию
|
||
|
||
Готовые дни повторно не собираются. Чтобы новый SQL точно выполнился, выберите
|
||
последний собранный день:
|
||
|
||
```sql
|
||
SELECT max(session_date)
|
||
FROM dds.session_v;
|
||
```
|
||
|
||
Сохраните десять строк этого дня без служебных колонок загрузки:
|
||
|
||
```sql
|
||
SELECT * EXCEPT (_load_id, _load_ts)
|
||
FROM dds.session_v
|
||
WHERE session_date = 'YYYY-MM-DD'
|
||
ORDER BY client_id, started_at
|
||
LIMIT 10;
|
||
```
|
||
|
||
Подставьте найденную дату и удалите только ее партицию. Команда идет под
|
||
служебным пользователем `etl`, потому что `analyst` имеет доступ только на
|
||
чтение:
|
||
|
||
```bash
|
||
docker compose exec -T clickhouse-01 sh -c \
|
||
'clickhouse-client --user etl --password "$CLICKHOUSE_ETL_PASSWORD" --query "$1"' \
|
||
_ "ALTER TABLE dds.session_rep ON CLUSTER clickstream_cluster DROP PARTITION 'YYYY-MM-DD'"
|
||
```
|
||
|
||
Откройте Airflow и запустите `etl_pipeline`. В даге `dds_transform` раскройте
|
||
группу `session`: в ней должны выполниться задачи `scope` и `rebuild`, а также
|
||
`replace` для выбранного дня. Если после третьей ступени мир ушел вперед,
|
||
Airflow создаст `replace` и для последующих прожитых дней. Следом конвейер
|
||
запустит витрины и `dq_check`.
|
||
|
||
Если прогон дошел до замены, но упал на проверке, ошибочная партиция уже
|
||
находится в DDS. После правки снова удалите тот же день перед следующим
|
||
запуском. Иначе расчет объема сочтет день готовым и пропустит вашу новую
|
||
сборку.
|
||
|
||
## Проверьте результат
|
||
|
||
Сначала повторите запрос с десятью строками. `session_id` и все деловые
|
||
колонки должны совпасть с сохраненным результатом. `_load_id` и `_load_ts`
|
||
изменятся: день собрал другой запуск.
|
||
|
||
Задача `dq_check.sessions_vs_reference` должна стать зеленой. Та же проверка
|
||
видна в хранилище:
|
||
|
||
```sql
|
||
SELECT data_date, reference_rows, actual_rows, failed_rows, status
|
||
FROM dm.dq_summary_v
|
||
WHERE check_name = 'sessions_vs_reference'
|
||
ORDER BY data_date;
|
||
```
|
||
|
||
Эта сверка сравнивает число сессий каждой куки за день с `VisitID`. Нулевой
|
||
`failed_rows` не подтверждает границы, страницы и UTM: две ошибки могут
|
||
оставить тот же счет. Возьмите поштучную сверку из третьей ступени. Для одной
|
||
куки выбранного дня сопоставьте границы `VisitID` по событиям с границами
|
||
строк `dds.session_v`. Проверьте начало, конец и атрибуты сессии.
|
||
|
||
Осталась физическая раскладка. Напишите отдельный запрос к
|
||
`dds.session_dist`. Для каждой строки он должен:
|
||
|
||
1. получить фактическую ноду через `hostName()`;
|
||
2. пересчитать ожидаемую ноду по `cityHash64(client_id)`;
|
||
3. посчитать строки, у которых ноды не совпали.
|
||
|
||
У кластера два равных шарда: остаток 0 соответствует `clickhouse-01`, остаток
|
||
1 — `clickhouse-02`. Сгруппируйте ответ по фактической ноде. На каждой ноде
|
||
число строк на чужом шарде должно быть нулевым. Счетная DQ-проверка читает
|
||
Distributed-таблицу и такую ошибку не заметит.
|
||
|
||
После зеленого результата запустите `etl_pipeline` еще раз, не удаляя
|
||
партицию. Расчет объема должен пропустить группу сессий: новых прожитых дней и
|
||
дыр в слое нет.
|
||
|
||
## Приемка
|
||
|
||
- [ ] переписаны только три файла `sql/dds/session_*.sql`, инфраструктура
|
||
упражнения не изменена;
|
||
- [ ] адресный прогон восстановил выбранный день и завершил
|
||
`etl_pipeline` успешно;
|
||
- [ ] сохраненные `session_id` и деловые колонки десяти строк совпали после
|
||
пересборки;
|
||
- [ ] `sessions_vs_reference` дает ноль расхождений во всех прожитых днях;
|
||
- [ ] для одной куки вручную сверены границы и атрибуты сессии;
|
||
- [ ] пересчет ключа по `hostName()` не нашел строк на чужом шарде;
|
||
- [ ] повторный прогон без новой дыры пропустил группу сессий.
|