Зачем: - Дать слою расчётную сущность, которой в потоке нет: стенд режет визиты сам, а VisitID генератора остаётся эталоном самопроверки. - Отделить прожитый день от живого — иначе визит, начатый в 23:50, замёрз бы обрезанным навсегда: пересобирать замёрзший день слой не станет. Что: - Добавлены dds.session_rep/_dist, двойник-донор dds.session_stage_rep/_dist и публичный договор dds.session_v. - Нарезка стоит на двух окнах: первое ставит шов там, где пауза длиннее тридцати минут, второе считает нарастающий итог швов — номер визита. Границу суток держит разбиение окна, отдельной проверки ей не нужно. - Порядок в окнах полный, по метке времени и event_id: сортировок две, и при равных метках они разошлись бы, разорвав сессию надвое. - Объём прогона считается от первой дыры в слое по последний прожитый день; прожитый день выводится из позиции мира и первого дня событий. Собирать нечего — группа задач пропускается, а не зеленеет впустую. - В dds_transform появилась группа session рядом с заказом; зависимости по данным между ними нет. - Потолок max_active_tasks: размноженная замена без него выносит планировщика за отведённые ему 640 МиБ. - parallel_distributed_insert_select у сессий не назначается вслух — ключ шардирования у источника и цели один по контракту ко-локации. - Записано, что позиция отвечает за «прожит», а не за «доехал»: сторож хвоста потока приезжает отдельным тикетом. Проверка: - make lint - make smoke (21 проверка), make check-clickhouse (10), make check-services - Стартовый мир, пересобранный с нуля: 79367 сессий за восемь дней; расхождение с uniqExact(VisitID) по паре «кука × день» — ноль; все 401185 событий ровно в одной сессии; session_id уникальны. - Ни одной паузы длиннее тридцати минут внутри сессии и ни одной пары соседних визитов куки, разделённой меньшим промежутком. - Повтор после DROP PARTITION ALL: те же session_id и то же содержимое строк. - Снесённая руками партиция 2026-06-04: следующий прогон взял хвост от неё, дни до дыры не тронуты. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
132 lines
6.3 KiB
SQL
132 lines
6.3 KiB
SQL
-- DDS: нарезка событий на визиты и сборка дней в таблицу-двойник.
|
|
--
|
|
-- Двойник очищается целиком в начале, дни собираются одной вставкой на весь
|
|
-- отрезок — по правилу заказа (sql/dds/order_rebuild.sql).
|
|
--
|
|
-- Нарезка — центральный расчёт слоя, и стоит она на двух окнах подряд.
|
|
-- Первое спрашивает у каждого события, сколько прошло с предыдущего события
|
|
-- той же куки в тот же день, и ставит единицу там, где пауза длиннее
|
|
-- таймаута: это шов между визитами. Второе считает нарастающий итог этих
|
|
-- единиц — номер визита внутри дня куки. Дальше визит становится обычной
|
|
-- группировкой.
|
|
--
|
|
-- Границу суток окна не проверяют: она уже в разбиении PARTITION BY
|
|
-- (client_id, event_date), поэтому визит через полночь не склеится и при
|
|
-- короткой паузе. Так правило визита ложится в запрос целиком: одна кука,
|
|
-- один день, паузы не длиннее таймаута (CONTEXT.md, «Визит»).
|
|
--
|
|
-- Первое событие куки за день сравнивается само с собой — третий аргумент
|
|
-- lagInFrame. Паузы у него нет, шов ему не нужен, и номер первого визита
|
|
-- получается нулём: номера различают визиты внутри дня куки, а не считают их
|
|
-- от единицы.
|
|
--
|
|
-- Порядок в окнах полный — метка времени и event_id. Окна считаются двумя
|
|
-- разными сортировками (видно в EXPLAIN PIPELINE), и при равных метках они
|
|
-- вправе разойтись: шов достался бы одной строке, а нарастающий итог другой,
|
|
-- и сессия разъехалась бы надвое.
|
|
--
|
|
-- Тридцать минут написаны числом, а не приехали параметром: это правило
|
|
-- аналитики, а не константа мира. Генератор режет визиты тем же порогом;
|
|
-- разойдись эти два числа — увидит счётная сверка с VisitID
|
|
-- (docs/architecture/dds/session.md, «Сверка с эталоном»).
|
|
|
|
ALTER TABLE dds.session_stage_rep ON CLUSTER clickstream_cluster DROP PARTITION ALL;
|
|
|
|
INSERT INTO dds.session_stage_dist
|
|
(
|
|
session_id,
|
|
client_id,
|
|
session_date,
|
|
started_at,
|
|
finished_at,
|
|
duration_seconds,
|
|
events_total,
|
|
pageviews,
|
|
cart_adds,
|
|
purchases,
|
|
entry_url,
|
|
exit_url,
|
|
utm_source,
|
|
utm_medium,
|
|
utm_campaign,
|
|
utm_content,
|
|
utm_term,
|
|
device_category,
|
|
region_city,
|
|
_load_id,
|
|
_load_ts
|
|
)
|
|
WITH
|
|
seams AS (
|
|
SELECT
|
|
client_id,
|
|
event_date,
|
|
event_time,
|
|
event_id,
|
|
event_type,
|
|
url,
|
|
utm_source,
|
|
utm_medium,
|
|
utm_campaign,
|
|
utm_content,
|
|
utm_term,
|
|
device_category,
|
|
region_city,
|
|
event_time - lagInFrame(event_time, 1, event_time) OVER visit > 1800
|
|
AS opens_visit
|
|
FROM dds.event_v
|
|
WHERE event_date BETWEEN {first_day:Date} AND {last_day:Date}
|
|
WINDOW visit AS (
|
|
PARTITION BY client_id, event_date
|
|
ORDER BY event_time, event_id
|
|
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
|
|
)
|
|
),
|
|
numbered AS (
|
|
SELECT
|
|
*,
|
|
sum(opens_visit) OVER (
|
|
PARTITION BY client_id, event_date
|
|
ORDER BY event_time, event_id
|
|
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
|
) AS visit_no
|
|
FROM seams
|
|
)
|
|
SELECT
|
|
-- Чекан визита: кука и его начало. Время входит в хеш секундой эпохи,
|
|
-- поэтому id держится за содержание сессии, а не за порядок сборки.
|
|
cityHash64(client_id, min(event_time)),
|
|
client_id,
|
|
event_date,
|
|
min(event_time),
|
|
max(event_time),
|
|
max(event_time) - min(event_time),
|
|
count(),
|
|
countIf(event_type = 'pageview'),
|
|
countIf(event_type = 'add_to_cart'),
|
|
countIf(event_type = 'purchase'),
|
|
argMin(url, event_time),
|
|
argMax(url, event_time),
|
|
-- Атрибуция и паспорт куки берутся с первого события визита. У паспорта
|
|
-- любое событие дало бы то же: устройство и город у куки постоянны.
|
|
argMin(utm_source, event_time),
|
|
argMin(utm_medium, event_time),
|
|
argMin(utm_campaign, event_time),
|
|
argMin(utm_content, event_time),
|
|
argMin(utm_term, event_time),
|
|
argMin(device_category, event_time),
|
|
argMin(region_city, event_time),
|
|
{load_id:String},
|
|
now64(3, 'UTC')
|
|
FROM numbered
|
|
GROUP BY client_id, event_date, visit_no
|
|
-- Капкана parallel_distributed_insert_select здесь нет, и это выигрыш
|
|
-- ко-локации: ключ шардирования у источника и цели один — cityHash64 от
|
|
-- куки, — поэтому визит ляжет туда же, где лежали его события, исполнись
|
|
-- вставка хоть на инициаторе, хоть локально на шардах. Заказу приходится
|
|
-- называть ноль вслух: совпадение ключей у него случайное
|
|
-- (sql/dds/order_rebuild.sql). Здесь выбор сделан обратный и он тоже
|
|
-- осознанный: настройку не называть, чтобы не притворяться, будто её значение
|
|
-- что-то решает.
|
|
SETTINGS distributed_foreground_insert = 1;
|