Зачем: - Дать слою расчётную сущность, которой в потоке нет: стенд режет визиты сам, а 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>
51 lines
4.1 KiB
SQL
51 lines
4.1 KiB
SQL
-- DDS: объём пересборки сессий — какие дни собирает этот запуск.
|
||
--
|
||
-- Ответ — две даты, границы непрерывного отрезка; список дней между ними
|
||
-- разворачивает даг. Пустой ответ означает пустой источник. Пустой первый
|
||
-- день — что собирать нечего: все прожитые дни уже в слое, и прогон честно
|
||
-- пропускает сборку.
|
||
--
|
||
-- Файл кончается без точки с запятой: почему — sql/dds/order_scope.sql.
|
||
--
|
||
-- Правый край — последний прожитый день, и знает его только позиция мира.
|
||
-- События живой день от прожитого не отличают: и посреди дня D, и сразу после
|
||
-- его конца max(EventDate) = D. А разница несущая — визит, начатый в 23:50,
|
||
-- посреди дня ещё идёт, и собранная сессия оказалась бы обрезанной навсегда:
|
||
-- пересобирать замёрзший день слой не станет
|
||
-- (docs/architecture/dds/session.md, «Движок и наполнение»).
|
||
--
|
||
-- Позиция — номер первого несыгранного дня, счётчик прожитого. Своей копии
|
||
-- начала оси стенд из-за неё не заводит: начало он спрашивает у данных, и
|
||
-- дата дня N в них — константа, начало оси плюс N. Держится якорь на том, что
|
||
-- ранние дни из ods.event не исчезают: срок жизни назначен сырью и таблицам
|
||
-- ошибок, самому событию — нет (sql/ddl/20-ods-tables.sql).
|
||
--
|
||
-- За «доехал» позиция не отвечает — только за «прожит». Дождаться хвоста
|
||
-- потока — работа отдельного шага готовности источника, и в даге его пока нет
|
||
-- (#156): до него недожёванный хвост даёт замёрзший недособранный день
|
||
-- (docs/architecture/etl.md, «Готовность источника»).
|
||
WITH
|
||
(SELECT min(EventDate) FROM ods.event_dist) AS first_source_day,
|
||
first_source_day + ({position:UInt16} - 1) AS last_lived_day,
|
||
-- Хвост считается от первой дыры, а не от последнего собранного дня:
|
||
-- замены партиций падают порознь, и счёт по максимуму собранного дня
|
||
-- дыру позади себя не заметил бы (доводы целиком — sql/dds/order_scope.sql).
|
||
-- У сессий это единственный способ дыру найти: дышащего окна, которое
|
||
-- пересобрало бы день заодно, здесь нет.
|
||
--
|
||
-- GLOBAL обязателен: обе таблицы распределённые, и обычный NOT IN сверял
|
||
-- бы дни источника с локальной частью слоя на каждом шарде.
|
||
-- minOrNull отличает «дыр нет» от даты по умолчанию.
|
||
(
|
||
SELECT minOrNull(day)
|
||
FROM (SELECT DISTINCT EventDate AS day FROM ods.event_dist)
|
||
WHERE day GLOBAL NOT IN (SELECT session_date FROM dds.session_dist)
|
||
) AS first_missing_day
|
||
SELECT
|
||
-- Живой день в слое отсутствует всегда, и работой это не считается:
|
||
-- дырой он станет, только когда мир его проживёт.
|
||
if(first_missing_day <= last_lived_day, first_missing_day, NULL) AS first_day,
|
||
last_lived_day AS last_day
|
||
-- FINAL не нужен: повтор доставки не двигает ни край оси, ни набор дней.
|
||
WHERE (SELECT count() FROM ods.event_dist) > 0
|