Зачем: выражения «собираются в донор» и «публикуются дни» требовали знать устройство конвейера до чтения объяснения. Что: описаны назначение промежуточных таблиц и последовательность расчета, записи и замены разделов. Перечитаны остальные объяснения: выбор дней, разворачивание позиций заказа, проверка сессий и повтор запуска. Проверка: make lint, git diff --check, локальные ссылки и якоря. SQL без комментариев и AST обоих дагов без строк документации не изменились. Refs: #249
170 lines
14 KiB
Markdown
170 lines
14 KiB
Markdown
# Модель сессии в DDS
|
||
|
||
Сессия объединяет события одного визита. На стенде вы учитесь определять
|
||
ее границы по событиям, вычислять устойчивый идентификатор и сверять
|
||
результат с эталоном. Поэтому сборка читает `dds.event_v`, где нет готового
|
||
`VisitID`. Этот идентификатор источника остается в ODS для проверки.
|
||
|
||
## Зерно
|
||
|
||
Зерно описывает, чему соответствует одна строка: здесь это один
|
||
[визит](../../../CONTEXT.md#люди-и-события). В него входят подряд идущие
|
||
события одного `client_id` без пауз длиннее 30 минут и без перехода через
|
||
модельную полночь. Пауза ровно в 30 минут новую сессию не открывает.
|
||
Учитываются все три типа событий: `pageview`, `add_to_cart`, `purchase`.
|
||
|
||
День сессии `session_date` берется из `event_date` событий DDS — это
|
||
`EventDate` источника, дата по часовому поясу счетчика. У всех событий
|
||
одной сессии она одинакова. Пересчитывать ее из времени UTC не нужно.
|
||
|
||
SQL рассчитывает границы визитов двумя оконными функциями: первая отмечает
|
||
начало нового визита, вторая по этим отметкам присваивает событиям номер
|
||
визита. Обе используют порядок `(event_time, event_id)`: при равном времени
|
||
события идут по идентификатору. Иначе два расчета могли бы по-разному
|
||
упорядочить события и отнести отметку начала к разным строкам.
|
||
|
||
## Идентификатор
|
||
|
||
`session_id` вычисляется как `cityHash64(client_id, min(event_time))`.
|
||
При повторной сборке тех же событий посетитель и начало визита остаются
|
||
прежними, поэтому идентификатор не меняется. Счетчик или случайный UUID
|
||
выдали бы другие значения.
|
||
|
||
По `session_id` можно сопоставить сессии до и после пересборки и назвать
|
||
конкретную строку при разборе расхождения. Он не используется для сортировки
|
||
или шардирования. У событий этого поля нет; связь с заказами строится
|
||
по событиям покупки.
|
||
|
||
## Состав строки
|
||
|
||
- `client_id`, `session_id`, `session_date` — посетитель, сессия и ее день.
|
||
- `started_at`, `finished_at`, `duration_seconds` — время первого и последнего
|
||
события и разница между ними в секундах. Времена хранятся в часовом поясе
|
||
счетчика.
|
||
- `events_total`, `pageviews`, `cart_adds`, `purchases` — число всех событий
|
||
и счетчики трех типов.
|
||
- `entry_url`, `exit_url` — страницы первого и последнего события.
|
||
- `utm_*` — метки первого события: по ним визит относят к рекламной кампании.
|
||
- `device_category`, `region_city` — устройство и город посетителя.
|
||
На стенде они постоянны для одной куки.
|
||
- `_load_id`, `_load_ts` — идентификатор запуска и время сборки строки.
|
||
|
||
В готовом SQL страницы выбираются через `argMin` и `argMax`, а UTM,
|
||
устройство и город — через `argMin`, только по `event_time`. При одинаковом
|
||
времени выбор между событиями не определен. Полный порядок окон нарезки
|
||
сам по себе эту неоднозначность не устраняет. Это ограничение учитывается
|
||
в [задании ступени 4](../../labs/4-1-session-pipeline.md).
|
||
|
||
В сессии нет суммы выручки: ее считают по заказам магазина. Клиентская
|
||
сумма покупки используется в сверке с заказами и в учебной UTM-витрине.
|
||
|
||
## Движок и наполнение
|
||
|
||
`dds.session_rep` хранит готовые сессии в `ReplicatedMergeTree` без колонки
|
||
версии. При чтении не нужно заново выполнять оконный расчет. Партиция —
|
||
раздел таблицы за один `session_date`; ключ сортировки — посетитель
|
||
и начало сессии. Читайте результат через `dds.session_v`: представление
|
||
над `dds.session_dist` явно перечисляет деловые и служебные колонки.
|
||
|
||
Источник сборки — `dds.event_v`. События, сессии и карта идентичностей
|
||
распределены по одному ключу `cityHash64(client_id)`: данные посетителя
|
||
находятся на одном шарде. Роли объектов и правило записи через `_dist`
|
||
описаны в [справочнике хранилища](../storage.md#раскладка-по-шардам).
|
||
|
||
Три SQL-файла выполняются последовательно в группе `session` дага
|
||
`dds_transform`:
|
||
|
||
1. `session_scope.sql` находит первую дату событий, за которую еще нет
|
||
сессий. Последний день для расчета — первый день событий ODS
|
||
плюс `world_position` минус один; `world_position` — номер следующего
|
||
дня для генерации. Так в расчет попадают только полностью
|
||
прожитые дни. При пустом ODS запрос не возвращает строк и задача падает;
|
||
если пропущенных прожитых дней нет, `first_day` равен `NULL` и группа
|
||
пропускается.
|
||
2. `session_rebuild.sql` очищает промежуточную таблицу
|
||
`dds.session_stage_rep`. Затем рассчитывает сессии выбранных дней
|
||
и записывает строки через `dds.session_stage_dist`. Пока идет расчет,
|
||
в основной таблице остаются прежние данные. Вставка ждет доставки
|
||
строк на шарды благодаря `distributed_foreground_insert = 1`.
|
||
3. `session_replace.sql` заменяет в `dds.session_rep` раздел за каждый
|
||
выбранный день готовым разделом `dds.session_stage_rep`. Для каждого
|
||
дня Airflow создает отдельную задачу `replace`.
|
||
|
||
Когда все события завершенного дня приняты в ODS, их набор на стенде
|
||
больше не меняется. Поэтому готовые сессии обычно не пересчитываются. Если пропущен ранний
|
||
день, диапазон включает и все последующие прожитые дни, даже уже собранные.
|
||
Если за день сохранилась хотя бы одна сессия, поиск пропущенных дней
|
||
не обнаружит потерю остальных. Условия повторного запуска и замены
|
||
разделов описаны в [справочнике конвейера](../etl.md#идемпотентность).
|
||
|
||
Сессий еще идущего дня в модели нет; его события доступны через
|
||
`dds.event_v`. Перед выбором дней даг ждет завершения доставки событий
|
||
в ODS — см. [готовность источника](../etl.md#готовность-источника).
|
||
|
||
## Сверка с эталоном
|
||
|
||
Проверка `sessions_vs_reference` в `dq_check` сравнивает для каждого
|
||
посетителя и дня число строк `dds.session_v` с `uniqExact(VisitID)`
|
||
из `ods.event_v`. Эталон читается из ODS, потому что `dds.event_v`
|
||
скрывает `VisitID`.
|
||
|
||
Нулевое число расхождений подтверждает только число сессий каждого
|
||
посетителя за день. Например, ошибочное объединение двух визитов
|
||
и разделение другого на два сохранят их общее число. Поэтому в заданиях
|
||
вы дополнительно сопоставляете границы сессий по исходным событиям и проверяете страницы,
|
||
UTM, устройство и город.
|
||
|
||
## Читатели
|
||
|
||
- Витрина обзора сессий `session_overview_v`: дневные итоги визитов в
|
||
срезе устройств; читает `dds.session_v` и поднимает зерно до дня — состав и довод
|
||
в ее [ТЗ](../../labs/1-3-session-overview.md) (#135).
|
||
- DQ-сверка с эталоном (см. выше).
|
||
- Лаба сессий: менти собирает нарезку сам до знакомства с кодом стенда,
|
||
потом сравнивает — с эталоном и со стендовой.
|
||
|
||
## Не входит
|
||
|
||
- Механика запуска трансформации и замены партиций на кластере —
|
||
[дневной конвейер](../etl.md).
|
||
- Контур проверок качества (даг DQ, `dm.dq_summary`) — отдельный разговор
|
||
этапа 4.
|
||
- Форма витрин над сессиями — #135.
|
||
|
||
## Отклонённые варианты
|
||
|
||
- **Готовый `VisitID` вместо своей нарезки.** Сборка выродилась бы в
|
||
`GROUP BY` по чужому полю: центральный урок слоя съеден, сверка с эталоном
|
||
осталась бы без предмета. Боевой довод «Метрика теперь отдаёт `VisitID` в
|
||
хитах» честно рассказывается в лабе — после того как менти собрал сам.
|
||
- **Гибрид: сборка по `VisitID`, DQ пересчитывает нарезку.** Сторож на
|
||
нарезку, которой сборка не делает; та же работа, что в варианте «считать
|
||
сам», но урок спрятан в проверку.
|
||
- **Одно динамическое представление вместо физической таблицы.** Оконная
|
||
сессионизация на каждое чтение;
|
||
урок «слой — контракт, а не копия» уже закреплён за `dds.event_v` — доводы
|
||
модели заказа, здесь они только сильнее. Публичное `dds.session_v` поверх
|
||
готовой физической модели ничего заново не считает.
|
||
- **Счётчик или UUID вместо детерминированного хеша.** Пересборка партиции
|
||
выдала бы другие id — замена перестала бы быть идемпотентной.
|
||
- **`VisitID` колонкой в строке сессии.** Чужой чекан в своей модели: наша
|
||
сессия обязана уметь расходиться с эталоном, на этом стоит сверка;
|
||
поштучное сопоставление — лаба, не таблица.
|
||
- **Массив посещённых страниц.** Вес без читателя: вход и выход — как у
|
||
визита Метрики, топы страниц считает витрина по событиям.
|
||
- **UTM последнего касания (как в v1).** Визит атрибутируется своим первым
|
||
хитом; последнее касание — атрибуция другого уровня, не сессионного.
|
||
- **Деньги в сессии.** Выручка — только по бэкенду; клиентская сумма живёт
|
||
в сверке (спека, разделы 4 и 7).
|
||
|
||
## Связанные решения
|
||
|
||
- [Модель заказа](order.md) — прецедент замены партиций в DDS и правило
|
||
служебных колонок слоя.
|
||
- [Дока хранилища](../storage.md) — раскладка по шардам, конвенции имён,
|
||
часовые пояса.
|
||
- Мастер-спека, разделы 1.2 и 7 — `VisitID` как эталон самопроверки
|
||
(принятое отступление приёмки), карта таблиц.
|
||
- [CONTEXT.md](../../../CONTEXT.md) — «Визит», «Граница суток», «День
|
||
активности».
|