Files
clickstream-data-platform/sql/ddl/50-dds-views.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

137 lines
5.5 KiB
SQL

-- DDS: публичный договор события на языке бизнес-модели.
--
-- Событие не копируется в DDS: обычное представление вторым уровнем читает
-- точное состояние из ods.event_v. Явный список колонок не пропускает в
-- договор служебные и сырые поля ODS при будущих изменениях источника.
--
-- Представление — договор чтения, не граница прав: роль bi_reader читает его
-- источник по решению docs/adr/0007-clickhouse-access.md.
--
-- event_date берётся напрямую из EventDate. Пересчёт из event_time помешал бы
-- первичному индексу отбирать день до чтения.
--
-- Europe/Samara задаёт пояс счётчика модельного мира. DDS отдаёт event_time
-- в этой временной линзе.
CREATE VIEW IF NOT EXISTS dds.event_v ON CLUSTER clickstream_cluster
AS
SELECT
WatchID AS event_id,
ClientID AS client_id,
CounterID AS counter_id,
EventDate AS event_date,
toTimeZone(UTCEventTime, 'Europe/Samara') AS event_time,
ClientTimeZone AS client_timezone,
EventType AS event_type,
URL AS url,
Referer AS referer,
Title AS title,
UTMSource AS utm_source,
UTMMedium AS utm_medium,
UTMCampaign AS utm_campaign,
UTMContent AS utm_content,
UTMTerm AS utm_term,
LastTrafficSource AS last_traffic_source,
HasGCLID AS has_gclid,
YCLID AS yclid,
Browser AS browser,
BrowserMajorVersion AS browser_major_version,
BrowserLanguage AS browser_language,
OperatingSystem AS operating_system,
OperatingSystemRoot AS operating_system_root,
transform(
DeviceCategory,
[1, 2, 3, 4],
['desktop', 'mobile', 'tablet', 'tv'],
toString(DeviceCategory)
) AS device_category,
MobilePhoneModel AS mobile_phone_model,
ScreenWidth AS screen_width,
ScreenHeight AS screen_height,
IPAddress AS ip_address,
RegionCountry AS region_country,
RegionCity AS region_city,
RegionCountryID AS region_country_id,
RegionCityID AS region_city_id,
GoalsReached AS goals_reached,
ParsedParamsKey1 AS parsed_params_key1,
purchaseID AS purchase_id,
purchaseRevenue AS purchase_revenue,
purchaseCurrency AS purchase_currency,
purchaseCoupon AS purchase_coupon,
productID AS product_id,
productName AS product_name,
productCategory AS product_category,
productPrice AS product_price,
productQuantity AS product_quantity,
productEventType AS product_event_type
FROM ods.event_v;
-- DDS: публичный договор заказа на языке бизнес-модели.
--
-- В отличие от события, у заказа под представлением лежит физическая модель:
-- разбор позиций и пересчёт пояса стоят денег, и платить их при каждом чтении
-- значило бы собирать модель заново на лету (docs/architecture/dds/order.md,
-- «Отклонённые варианты»). Представление здесь ничего не вычисляет — оно
-- только называет колонки, которые слой обещает читателю.
--
-- FINAL нет и не будет: строка на заказ одна по построению, повтор гасит
-- замена партиции, а не колонка версии.
--
-- Служебные колонки перечислены наравне с деловыми — это координаты
-- собственной загрузки слоя, и расследование происшествия начинается с них:
-- по _load_id видно, какой запуск собрал день.
CREATE VIEW IF NOT EXISTS dds.order_v ON CLUSTER clickstream_cluster
AS
SELECT
order_id,
user_id,
status,
order_date,
created_at,
updated_at,
items_total,
discount,
delivery,
total,
item_sku,
item_quantity,
item_price,
_load_id,
_load_ts
FROM dds.order_dist;
-- DDS: публичный договор сессии на языке бизнес-модели.
--
-- Под представлением — физическая модель, как у заказа: оконная нарезка
-- визитов на каждое чтение означала бы считать сущность заново в каждом
-- отчёте (docs/architecture/dds/session.md, «Отклонённые варианты»).
--
-- Эталонного VisitID в договоре нет намеренно: наша нарезка обязана уметь
-- разойтись с эталоном, на этом стоит сверка. За эталоном она ходит в
-- ods.event_v.
CREATE VIEW IF NOT EXISTS dds.session_v ON CLUSTER clickstream_cluster
AS
SELECT
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
FROM dds.session_dist;