Files
clickstream-data-platform/sql/ddl/45-dds-tables.sql
T
ddadminandClaude Opus 5 176cf5520d feat(dds): добавлена модель сессии и нарезка визитов окнами
Зачем:
- Дать слою расчётную сущность, которой в потоке нет: стенд режет визиты сам,
  а 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>
2026-08-26 07:56:44 -04:00

173 lines
12 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- DDS: физические модели слоя и доноры их партиций.
--
-- Идемпотентность здесь устроена иначе, чем в приёме, и это главный урок обеих
-- сущностей файла. В ODS повтор гасит колонка версии:
-- ReplacingMergeTree(updated_at) оставляет позднюю строку. Здесь колонки
-- версии нет вовсе — повтор гасит замена партиции целиком: день собирается
-- заново и подменяется одним атомарным движением. Поэтому строка таблицы
-- ровно одна на своё зерно, а чтению не нужен FINAL.
-- Локальная таблица заказа.
--
-- Зерно — заказ в текущем состоянии, ключ order_id. Историю версий слой не
-- хранит: у неё нет читателя ни в одной витрине, а ODS её и не обещает
-- (docs/architecture/dds/order.md, «Зерно»).
--
-- Партиция — order_date, день заказа в поясе счётчика. Партиция дневная не
-- ради размера куска, а потому что день — единица пересборки: пока заказ
-- внутри окна изменяемости, его день «дышит» и собирается заново, за окном
-- замерзает. Ключ партиции и есть контракт трансформации с хранилищем.
--
-- Ключ сортировки — order_id: единственный устойчивый ключ сущности, он же
-- ключ шардирования ниже. Колонки версии в движке нет: см. шапку файла.
CREATE TABLE IF NOT EXISTS dds.order_rep ON CLUSTER clickstream_cluster
(
order_id String,
user_id UInt64,
status LowCardinality(String),
-- День заказа посчитан один раз при загрузке, по образцу EventDate у
-- события: суточные срезы группируются по готовой дате и пояса не
-- упоминают (docs/architecture/storage.md, «Часовые пояса»).
order_date Date,
-- Времена лежат местными: DDS обслуживает человека с дашбордом, и пояс
-- пересчитан один раз здесь, а не в каждом отчёте. Линза названа в типе,
-- поэтому пояс сервера на эти колонки не влияет.
created_at DateTime64(3, 'Europe/Samara'),
updated_at DateTime64(3, 'Europe/Samara'),
items_total Decimal(18, 2),
discount Decimal(18, 2),
delivery Decimal(18, 2),
total Decimal(18, 2),
-- Позиции — три массива одной длины: артикул, количество, цена. Родная
-- для колоночного хранилища денормализация: позиция не живёт без заказа и
-- своего ключа не имеет, а разворачивает массивы читатель — ARRAY JOIN в
-- витрине выручки. Форма записи выбрана по дому: широкое событие хранит
-- повторяющиеся группы Метрики такими же параллельными колонками
-- (productID, productPrice, productQuantity в ods.event_rep), а не Nested.
item_sku Array(String),
item_quantity Array(UInt32),
item_price Array(Decimal(18, 2)),
-- Координаты собственной загрузки слоя: каким запуском и когда собран
-- день. При замене партиции они одинаковы у всех строк дня. Ярлыки ODS
-- сюда не переезжают — почему, разобрано в доке модели.
_load_id String,
_load_ts DateTime64(3, 'UTC')
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
PARTITION BY order_date
ORDER BY order_id;
-- Лицо загрузки и диагностики. Ключ шардирования — cityHash64(order_id), тот
-- же, что у ods.order_dist: заказ остаётся на одном шарде на всём пути через
-- слои, и соединения по зерну заказа обходятся без GLOBAL
-- (docs/architecture/storage.md, «Раскладка по шардам»).
CREATE TABLE IF NOT EXISTS dds.order_dist ON CLUSTER clickstream_cluster
AS dds.order_rep
ENGINE = Distributed('clickstream_cluster', 'dds', 'order_rep', cityHash64(order_id));
-- Донор партиций: таблица-двойник, в которой день собирается перед подменой.
--
-- Двойник нужен потому, что REPLACE PARTITION берёт готовый кусок из другой
-- таблицы, а не из результата запроса. Собрать день прямо в цель нельзя:
-- вставка добавила бы строки к уже лежащим, и повтор прогона удвоил бы день.
--
-- Форма двойника задана правилом донора: структура, ключ партиции, ключ
-- сортировки, первичный ключ и политика хранения обязаны совпадать с целью —
-- это требование самой операции (проверено по документации ClickHouse,
-- sql-reference/statements/alter/partition). Отсюда AS dds.order_rep вместо
-- повторённого списка колонок: расхождение становится невозможным по
-- построению и не держится на внимательности того, кто правит.
--
-- Своё имя в keeper двойник получает от макроса {table}, поэтому две таблицы
-- не делят путь репликации.
CREATE TABLE IF NOT EXISTS dds.order_stage_rep ON CLUSTER clickstream_cluster
AS dds.order_rep
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
PARTITION BY order_date
ORDER BY order_id;
-- Двойник шардируется тем же ключом, что цель. REPLACE PARTITION идёт по
-- локальным таблицам, шард за шардом. Ляг строка заказа в
-- доноре на другой шард, чем её место в цели, — подмена перенесла бы её не
-- туда, и заказ пропал бы с одного шарда и задвоился на другом.
CREATE TABLE IF NOT EXISTS dds.order_stage_dist ON CLUSTER clickstream_cluster
AS dds.order_stage_rep
ENGINE = Distributed('clickstream_cluster', 'dds', 'order_stage_rep', cityHash64(order_id));
-- Локальная таблица сессии.
--
-- Зерно — визит: подряд идущие события одной куки без пауз длиннее тридцати
-- минут, не пересекающие границу суток (CONTEXT.md, «Визит»). Сущность
-- расчётная, в потоке её нет — слой режет и чеканит её сам
-- (docs/architecture/dds/session.md).
--
-- Партиция — день сессии. У заказа день дышит внутри окна изменяемости, у
-- сессии замерзает сразу: визит суток не пересекает, поэтому прожитый день
-- собирается один раз и целиком. Отсюда и цена отказа: пропущенный день
-- заказа вернуло бы в работу следующее окно, а день сессии вернёт только счёт
-- хвоста от первой дыры (sql/dds/session_scope.sql).
--
-- Ключ сортировки — кука и начало сессии: тем же порядком идёт и нарезка, и
-- чтение «визиты посетителя подряд». session_id в ключ не входит: он ручка
-- для витрин и разбора расхождений, а не адрес строки.
CREATE TABLE IF NOT EXISTS dds.session_rep ON CLUSTER clickstream_cluster
(
-- Суррогат слоя: cityHash64 от куки и начала сессии. Детерминированность
-- несущая — пересборка партиции обязана отчеканить те же id
-- (docs/architecture/dds/session.md, «Идентификатор»).
session_id UInt64,
client_id UInt64,
session_date Date,
started_at DateTime('Europe/Samara'),
finished_at DateTime('Europe/Samara'),
duration_seconds UInt32,
-- Счётчики по типам событий: у визита Метрики на этом месте достигнутые
-- цели, которых стенд не шлёт.
events_total UInt32,
pageviews UInt32,
cart_adds UInt32,
purchases UInt32,
-- Вход и выход визита, как StartURL и EndURL у Метрики. Полного списка
-- страниц нет и у неё: топы считает витрина по событиям.
entry_url String,
exit_url String,
-- Атрибуция визита — метки его первого события.
utm_source String,
utm_medium String,
utm_campaign String,
utm_content String,
utm_term String,
-- Паспорт куки: устройство и город приписаны ей на всю жизнь, поэтому
-- берутся с первого события и внутри визита не меняются.
device_category LowCardinality(String),
region_city String,
_load_id String,
_load_ts DateTime64(3, 'UTC')
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
PARTITION BY session_date
ORDER BY (client_id, started_at);
-- Лицо загрузки и диагностики. Ключ шардирования — cityHash64(client_id), то
-- же выражение, что у событий и у карты идентичностей
-- (docs/architecture/storage.md, «Раскладка по шардам»). Ко-локация здесь не
-- только про соединения: на ней стоит сама сборка — все события куки лежат на
-- одном шарде, и нарезать их можно, не собирая куку с двух нод
-- (sql/dds/session_rebuild.sql).
CREATE TABLE IF NOT EXISTS dds.session_dist ON CLUSTER clickstream_cluster
AS dds.session_rep
ENGINE = Distributed('clickstream_cluster', 'dds', 'session_rep', cityHash64(client_id));
-- Донор партиций сессии. Правило донора — в комментарии к dds.order_stage_rep
-- выше: структура и ключи обязаны совпадать с целью, отсюда AS dds.session_rep
-- и тот же ключ шардирования.
CREATE TABLE IF NOT EXISTS dds.session_stage_rep ON CLUSTER clickstream_cluster
AS dds.session_rep
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
PARTITION BY session_date
ORDER BY (client_id, started_at);
CREATE TABLE IF NOT EXISTS dds.session_stage_dist ON CLUSTER clickstream_cluster
AS dds.session_stage_rep
ENGINE = Distributed('clickstream_cluster', 'dds', 'session_stage_rep', cityHash64(client_id));