docs(warehouse): упрощены объяснения витрин, сессий и пересчета
Зачем: читателю ступеней 1–4 нужно проследить выбор дней, расчет строки и публикацию результата без расшифровки метафор. Что: согласованы справочники DM, сессий, ETL и два раздела хранилища с комментариями шести SQL-примеров. Уточнены ограничения повторного запуска и выбора атрибутов при равном времени событий. Проверка: make lint, git diff --check, локальные Markdown-ссылки и якоря. SQL без комментариев совпадает; AST Python без строк документации совпадает. Исторические разделы и материалы вне границ задачи сохранены. Refs: #249
This commit is contained in:
1 parent
84dc718f62
commit
5f317ba8d2
11 files changed
+440
-490
No files matched your search
@@ -1,111 +1,113 @@
|
||||
# Модель сессии в DDS
|
||||
|
||||
Учебный результат: менти строит расчётную сущность, которой нет в потоке, —
|
||||
режет события на сессии по правилу визита, чеканит идентификатор и сверяет
|
||||
свою нарезку с эталоном. Стенд здесь играет роль сессионизатора: в бою её
|
||||
играет Метрика, которая режет визиты у себя и с октября 2025 отдаёт готовый
|
||||
`VisitID` прямо в хитах. Поэтому стенд считает сессии сам, а `VisitID`
|
||||
генератора использует только как эталон самопроверки — иначе сборка сессий
|
||||
выродилась бы в `GROUP BY` по чужому полю и центральный урок
|
||||
слоя пропал бы. Без этого абзаца действия стенда выглядят странно: «зачем
|
||||
считать то, что уже дано колонкой».
|
||||
Сессия объединяет события одного визита. На стенде вы учитесь определять
|
||||
ее границы по событиям, вычислять устойчивый идентификатор и сверять
|
||||
результат с эталоном. Поэтому сборка читает `dds.event_v`, где нет готового
|
||||
`VisitID`. Этот идентификатор источника остается в ODS для проверки.
|
||||
|
||||
## Зерно
|
||||
|
||||
Одна строка — один визит ([CONTEXT.md](../../../CONTEXT.md)): подряд идущие
|
||||
события одной куки без пауз длиннее 30 минут, не пересекающие границу суток.
|
||||
В нарезку входят все три типа событий — торговые садятся на свои страницы
|
||||
и держат сессию наравне с просмотрами. События внутри сессии
|
||||
упорядочиваются по метке времени события (`UTCEventTime` — опорная колонка
|
||||
приёма).
|
||||
Зерно описывает, чему соответствует одна строка: здесь это один
|
||||
[визит](../../../CONTEXT.md#люди-и-события). В него входят подряд идущие
|
||||
события одного `client_id` без пауз длиннее 30 минут и без перехода через
|
||||
модельную полночь. Пауза ровно в 30 минут новую сессию не открывает.
|
||||
Учитываются все три типа событий: `pageview`, `add_to_cart`, `purchase`.
|
||||
|
||||
Граница суток проходит по модельному дню счётчика, и он у события уже есть:
|
||||
`EventDate` — свойство содержимого. Раз сессия сутки не пересекает, у всех
|
||||
её событий `EventDate` один; он и становится **днём сессии** — колонкой
|
||||
партиции, без пересчёта.
|
||||
День сессии `session_date` берется из `event_date` событий DDS — это
|
||||
`EventDate` источника, дата по часовому поясу счетчика. У всех событий
|
||||
одной сессии она одинакова. Пересчитывать ее из времени UTC не нужно.
|
||||
|
||||
Оба окна нарезки используют порядок `(event_time, event_id)`: при равном
|
||||
времени события идут по идентификатору. Одинаковый порядок нужен, чтобы
|
||||
признак начала визита и накопленный номер визита относились к одним
|
||||
и тем же событиям.
|
||||
|
||||
## Идентификатор
|
||||
|
||||
Идентификатор сессии в природе не существует: сессия — расчётная сущность,
|
||||
и id ей чеканит тот, кто сессионизирует. `VisitID` — такой же суррогат,
|
||||
отчеканенный генератором в роли Метрики; наш — `session_id`,
|
||||
детерминированный хеш от пары «кука + начало сессии» (`cityHash64`).
|
||||
Детерминированность обязательна: партиция дня пересобирается заменой, и
|
||||
повторный прогон обязан отчеканить те же id — счётчик или UUID это правило
|
||||
ломают.
|
||||
`session_id` вычисляется как `cityHash64(client_id, min(event_time))`.
|
||||
При повторной сборке тех же событий посетитель и начало визита остаются
|
||||
прежними, поэтому идентификатор не меняется. Счетчик или случайный UUID
|
||||
выдали бы другие значения.
|
||||
|
||||
`session_id` — не ключ сортировки и не ключ шардирования, а колонка-ручка:
|
||||
в соединениях он не участвует — события его не несут, мост к заказам живёт
|
||||
на событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со
|
||||
стендовой и назвать расхождение одним числом) и разбор DQ-расхождений.
|
||||
По `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` — идентификатор запуска и время сборки строки.
|
||||
|
||||
- **ключи и времена**: кука, `session_id`, начало и конец сессии,
|
||||
длительность, день сессии. Времена — в поясе счётчика по правилу слоя;
|
||||
- **счётчики**: событий всего, просмотров, корзин, покупок;
|
||||
- **страницы**: входная и выходная — как `StartURL`/`EndURL` визита; полного
|
||||
списка страниц нет и у Метрики, топы считаются по событиям;
|
||||
- **атрибуция**: UTM-метки первого события сессии — так визит атрибутирует
|
||||
Метрика. В v1 витрина брала последнее касание — заменено осознанно;
|
||||
- **паспорт куки**: категория устройства (расшифрованная, язык бизнес-модели)
|
||||
и город — у куки они постоянны, берутся с первого события;
|
||||
- **служебные**: `_load_id` и `_load_ts` собравшего запуска — правило слоя
|
||||
([модель заказа](order.md), «Служебные колонки»).
|
||||
В готовом SQL страницы выбираются через `argMin` и `argMax`, а UTM,
|
||||
устройство и город — через `argMin`, только по `event_time`. При одинаковом
|
||||
времени выбор между событиями не определен. Полный порядок окон нарезки
|
||||
сам по себе эту неоднозначность не устраняет. Это ограничение учитывается
|
||||
в [задании ступени 4](../../labs/4-1-session-pipeline.md).
|
||||
|
||||
Осознанные расхождения с образцом: **денег в сессии нет** — визит Метрики
|
||||
несёт клиентскую выручку, у нас деньги считает только бэкенд (спека, раздел
|
||||
7), а клиентская сумма живёт в сверке; **целей нет** — их место заняли
|
||||
счётчики торговых событий.
|
||||
В сессии нет суммы выручки: ее считают по заказам магазина. Клиентская
|
||||
сумма покупки используется в сверке с заказами и в учебной UTM-витрине.
|
||||
|
||||
## Движок и наполнение
|
||||
|
||||
`dds.session_rep` — `ReplicatedMergeTree` без колонки версии, по образцу
|
||||
заказа: партиция — день сессии, ключ сортировки — кука и начало сессии.
|
||||
`dds.session_dist` шардирует по `cityHash64(client_id)`, тем же выражением,
|
||||
что события, — это ко-локация с `dds.identity_map_rep` ([дока
|
||||
хранилища](../storage.md), «Раскладка по шардам»).
|
||||
Совпадение ключа с событиями не случайно: все события куки лежат на одном
|
||||
шарде, поэтому сессионизация локальна по природе — капкан смены ключа при
|
||||
вставке, который дока хранилища обещает сборкам DDS, у сессий не стреляет.
|
||||
`dds.session_rep` хранит готовые сессии в `ReplicatedMergeTree` без колонки
|
||||
версии. При чтении не нужно заново выполнять оконный расчет. Партиция —
|
||||
раздел таблицы за один `session_date`; ключ сортировки — посетитель
|
||||
и начало сессии. Читайте результат через `dds.session_v`: представление
|
||||
над `dds.session_dist` явно перечисляет деловые и служебные колонки.
|
||||
|
||||
Сборка — дневной батч: хранилище выше ODS по смыслу батчевое, раз второй
|
||||
источник приходит раз в сутки. Собирается день, который мир уже прожил, и
|
||||
собирается один раз и целиком; какой день прожит, слой спрашивает у позиции
|
||||
мира ([дневной конвейер](../etl.md), «Объём прогона»). Идемпотентность держит
|
||||
замена партиции. В отличие от заказов, где батч пересобирает до K «дышащих»
|
||||
дней, у сессии дышащего окна нет: прожитый день самодостаточен (правило
|
||||
спеки, раздел 6). Второй раз он соберётся только
|
||||
вынужденно — когда упавшая замена оставила позади дыру и хвост от неё накрыл
|
||||
соседние дни. Дней в одном прогоне при этом бывает несколько — столько,
|
||||
сколько мир успел прожить без слоя. Внутри живого дня
|
||||
модель сессии про сегодня честно молчит — «сегодня» смотрят через
|
||||
`dds.event_v`; сессия и по смыслу не финальна, пока день не прожит.
|
||||
Источник сборки — `dds.event_v`. События, сессии и карта идентичностей
|
||||
распределены по одному ключу `cityHash64(client_id)`: данные посетителя
|
||||
находятся на одном шарде. Роли объектов и правило записи через `_dist`
|
||||
описаны в [справочнике хранилища](../storage.md#раскладка-по-шардам).
|
||||
|
||||
Публичная поверхность — `dds.session_v`: обычное представление с явным
|
||||
списком деловых и собственных служебных колонок над `dds.session_dist`.
|
||||
Роли объектов и доступ следуют общей [конвенции](../storage.md), «Имена
|
||||
объектов».
|
||||
Три SQL-файла выполняются последовательно в группе `session` дага
|
||||
`dds_transform`:
|
||||
|
||||
Источник сборки — `dds.event_v` ([контракт события](event.md)): сессии
|
||||
нужны от него ключи нарезки, тип события, страница, UTM и паспорт куки.
|
||||
Механика запуска — где собирается партиция и как подменяется на кластере —
|
||||
не входит: это устройство [дневного конвейера](../etl.md).
|
||||
1. `session_scope.sql` находит первый день событий, которого еще нет
|
||||
в таблице сессий. Последний день для сборки — первый день событий ODS
|
||||
плюс `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` партицией
|
||||
донора. Для каждого дня Airflow создает отдельную задачу `replace`.
|
||||
|
||||
В обычном ходе сессии прожитого дня больше не меняются. Если пропущен ранний
|
||||
день, диапазон включает и все последующие прожитые дни, даже уже собранные.
|
||||
Поиск по наличию даты не обнаружит неполный день, у которого часть строк
|
||||
сохранилась. Условия повторного запуска и границы атомарности замены
|
||||
описаны в [справочнике конвейера](../etl.md#идемпотентность).
|
||||
|
||||
Сессий еще идущего дня в модели нет; его события доступны через
|
||||
`dds.event_v`. Перед выбором дней даг ждет завершения доставки событий
|
||||
в ODS — см. [готовность источника](../etl.md#готовность-источника).
|
||||
|
||||
## Сверка с эталоном
|
||||
|
||||
Счётная DQ-проверка с зерном «кука × день»: число сессий нашей нарезки
|
||||
равно `uniqExact(VisitID)` по тем же событиям. Счёт по Distributed ловит и
|
||||
склеенную, и разрезанную сессию — мимо него проходит лишь взаимная
|
||||
компенсация ошибок внутри одной куки за день; эталон в модель не тащим —
|
||||
строка сессии остаётся нашей, а не генераторной. Поштучное сопоставление
|
||||
границ — материал лабы, где менти разбирает найденное расхождение руками. Дом проверки —
|
||||
контур DQ этапа 4. Сверке нужен `VisitID`, а сборке сессий — нет; контракт
|
||||
`dds.event_v` его скрывает, и за эталоном проверка идёт в `ods.event_v`
|
||||
([контракт события](event.md)).
|
||||
Проверка `sessions_vs_reference` в `dq_check` сравнивает для каждого
|
||||
посетителя и дня число строк `dds.session_v` с `uniqExact(VisitID)`
|
||||
из `ods.event_v`. Эталон читается из ODS, потому что `dds.event_v`
|
||||
скрывает `VisitID`.
|
||||
|
||||
Нулевое число расхождений подтверждает совпадение счета, но не всех
|
||||
границ и атрибутов сессий. Две ошибки внутри одного посетителя и дня
|
||||
могут сохранить число визитов. Поэтому в заданиях вы дополнительно
|
||||
сопоставляете границы сессий по исходным событиям и проверяете страницы,
|
||||
UTM, устройство и город.
|
||||
|
||||
## Читатели
|
||||
|
||||
|
||||
Reference in new issue
Block a user