Зачем: - Дать слою расчётную сущность, которой в потоке нет: стенд режет визиты сам, а 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>
137 lines
5.5 KiB
SQL
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;
|