- Зачем:
- в docs/labs/ будет много заданий; номер ступени и номер задания в имени
делают папку оглавлением программы и избавляют от поиска по README (#230).
- Что:
- шесть ТЗ переименованы через git mv: 1-1…1-5 для ступени 1,
2-1-superset-chart.md для ступени 2; факультатив superset-dashboard.md
остался без номера.
- обновлены ссылки в docs/labs/README.md, 2-1-superset-chart.md,
docs/architecture/dds/session.md и пути в dags/homework_check.py.
- правило имен для docs/labs/ в AGENTS.md переписано под S-N-slug.
- Проверка:
- make lint; скрипт обхода относительных ссылок в docs/ и AGENTS.md —
битых нет.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
163 lines
14 KiB
Markdown
163 lines
14 KiB
Markdown
# Модель сессии в DDS
|
|
|
|
Учебный результат: менти строит расчётную сущность, которой нет в потоке, —
|
|
режет события на сессии по правилу визита, чеканит идентификатор и сверяет
|
|
свою нарезку с эталоном. Стенд здесь играет роль сессионизатора: в бою её
|
|
играет Метрика, которая режет визиты у себя и с октября 2025 отдаёт готовый
|
|
`VisitID` прямо в хитах. Поэтому стенд считает сессии сам, а `VisitID`
|
|
генератора использует только как эталон самопроверки — иначе сборка сессий
|
|
выродилась бы в `GROUP BY` по чужому полю и центральный урок
|
|
слоя пропал бы. Без этого абзаца действия стенда выглядят странно: «зачем
|
|
считать то, что уже дано колонкой».
|
|
|
|
## Зерно
|
|
|
|
Одна строка — один визит ([CONTEXT.md](../../../CONTEXT.md)): подряд идущие
|
|
события одной куки без пауз длиннее 30 минут, не пересекающие границу суток.
|
|
В нарезку входят все три типа событий — торговые садятся на свои страницы
|
|
и держат сессию наравне с просмотрами. События внутри сессии
|
|
упорядочиваются по метке времени события (`UTCEventTime` — опорная колонка
|
|
приёма).
|
|
|
|
Граница суток проходит по модельному дню счётчика, и он у события уже есть:
|
|
`EventDate` — свойство содержимого. Раз сессия сутки не пересекает, у всех
|
|
её событий `EventDate` один; он и становится **днём сессии** — колонкой
|
|
партиции, без пересчёта.
|
|
|
|
## Идентификатор
|
|
|
|
Идентификатор сессии в природе не существует: сессия — расчётная сущность,
|
|
и id ей чеканит тот, кто сессионизирует. `VisitID` — такой же суррогат,
|
|
отчеканенный генератором в роли Метрики; наш — `session_id`,
|
|
детерминированный хеш от пары «кука + начало сессии» (`cityHash64`).
|
|
Детерминированность обязательна: партиция дня пересобирается заменой, и
|
|
повторный прогон обязан отчеканить те же id — счётчик или UUID это правило
|
|
ломают.
|
|
|
|
`session_id` — не ключ сортировки и не ключ шардирования, а колонка-ручка:
|
|
в соединениях он не участвует — события его не несут, мост к заказам живёт
|
|
на событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со
|
|
стендовой и назвать расхождение одним числом) и разбор DQ-расхождений.
|
|
|
|
## Состав строки
|
|
|
|
Боевой образец — таблица визитов Метрики, парная к нашему формату хита; по
|
|
ней состав и собран:
|
|
|
|
- **ключи и времена**: кука, `session_id`, начало и конец сессии,
|
|
длительность, день сессии. Времена — в поясе счётчика по правилу слоя;
|
|
- **счётчики**: событий всего, просмотров, корзин, покупок;
|
|
- **страницы**: входная и выходная — как `StartURL`/`EndURL` визита; полного
|
|
списка страниц нет и у Метрики, топы считаются по событиям;
|
|
- **атрибуция**: UTM-метки первого события сессии — так визит атрибутирует
|
|
Метрика. В v1 витрина брала последнее касание — заменено осознанно;
|
|
- **паспорт куки**: категория устройства (расшифрованная, язык бизнес-модели)
|
|
и город — у куки они постоянны, берутся с первого события;
|
|
- **служебные**: `_load_id` и `_load_ts` собравшего запуска — правило слоя
|
|
([модель заказа](order.md), «Служебные колонки»).
|
|
|
|
Осознанные расхождения с образцом: **денег в сессии нет** — визит Метрики
|
|
несёт клиентскую выручку, у нас деньги считает только бэкенд (спека, раздел
|
|
7), а клиентская сумма живёт в сверке; **целей нет** — их место заняли
|
|
счётчики торговых событий.
|
|
|
|
## Движок и наполнение
|
|
|
|
`dds.session_rep` — `ReplicatedMergeTree` без колонки версии, по образцу
|
|
заказа: партиция — день сессии, ключ сортировки — кука и начало сессии.
|
|
`dds.session_dist` шардирует по `cityHash64(client_id)`, тем же выражением,
|
|
что события, — это ко-локация с `dds.identity_map_rep` ([дока
|
|
хранилища](../storage.md), «Раскладка по шардам»).
|
|
Совпадение ключа с событиями не случайно: все события куки лежат на одном
|
|
шарде, поэтому сессионизация локальна по природе — капкан смены ключа при
|
|
вставке, который дока хранилища обещает сборкам DDS, у сессий не стреляет.
|
|
|
|
Сборка — дневной батч: хранилище выше ODS по смыслу батчевое, раз второй
|
|
источник приходит раз в сутки. Собирается день, который мир уже прожил, и
|
|
собирается один раз и целиком; какой день прожит, слой спрашивает у позиции
|
|
мира ([дневной конвейер](../etl.md), «Объём прогона»). Идемпотентность держит
|
|
замена партиции. В отличие от заказов, где батч пересобирает до K «дышащих»
|
|
дней, у сессии дышащего окна нет: прожитый день самодостаточен (правило
|
|
спеки, раздел 6). Второй раз он соберётся только
|
|
вынужденно — когда упавшая замена оставила позади дыру и хвост от неё накрыл
|
|
соседние дни. Дней в одном прогоне при этом бывает несколько — столько,
|
|
сколько мир успел прожить без слоя. Внутри живого дня
|
|
модель сессии про сегодня честно молчит — «сегодня» смотрят через
|
|
`dds.event_v`; сессия и по смыслу не финальна, пока день не прожит.
|
|
|
|
Публичная поверхность — `dds.session_v`: обычное представление с явным
|
|
списком деловых и собственных служебных колонок над `dds.session_dist`.
|
|
Роли объектов и доступ следуют общей [конвенции](../storage.md), «Имена
|
|
объектов».
|
|
|
|
Источник сборки — `dds.event_v` ([контракт события](event.md)): сессии
|
|
нужны от него ключи нарезки, тип события, страница, UTM и паспорт куки.
|
|
Механика запуска — где собирается партиция и как подменяется на кластере —
|
|
не входит: это устройство [дневного конвейера](../etl.md).
|
|
|
|
## Сверка с эталоном
|
|
|
|
Счётная DQ-проверка с зерном «кука × день»: число сессий нашей нарезки
|
|
равно `uniqExact(VisitID)` по тем же событиям. Счёт по Distributed ловит и
|
|
склеенную, и разрезанную сессию — мимо него проходит лишь взаимная
|
|
компенсация ошибок внутри одной куки за день; эталон в модель не тащим —
|
|
строка сессии остаётся нашей, а не генераторной. Поштучное сопоставление
|
|
границ — материал лабы, где менти разбирает найденное расхождение руками. Дом проверки —
|
|
контур DQ этапа 4. Сверке нужен `VisitID`, а сборке сессий — нет; контракт
|
|
`dds.event_v` его скрывает, и за эталоном проверка идёт в `ods.event_v`
|
|
([контракт события](event.md)).
|
|
|
|
## Читатели
|
|
|
|
- Витрина обзора сессий `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) — «Визит», «Граница суток», «День
|
|
активности».
|