- Зачем: - реализованы требования issue #153 к мосту между кукой и пользователем. - Что: - добавлены физическая карта, точное представление и полный пересчёт через GLOBAL JOIN. - задача identity_map поставлена после сборки заказов в dds_transform. - устройство и живые проверки карты записаны в документации хранилища. - Проверка: - make lint, make smoke, make check-clickhouse, make check-services. - три запуска dds_transform: 4770 физических строк и 1590 уникальных пар в _v.
199 lines
13 KiB
SQL
199 lines
13 KiB
SQL
-- DDS: физические модели слоя и доноры партиций.
|
||
--
|
||
-- Заказ и сессия гасят повтор заменой дневной партиции, без колонки версии.
|
||
-- Карта идентичностей пересчитывается целиком и пишет новую версию пары.
|
||
|
||
-- Локальная таблица заказа.
|
||
--
|
||
-- Зерно — заказ в текущем состоянии, ключ 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));
|
||
|
||
-- DDS: известная связь куки трекера с пользователем магазина.
|
||
--
|
||
-- Полный запуск снова пишет все известные пары. Физические повторы до
|
||
-- фонового слияния допустимы: _load_ts выбирает последнюю версию пары.
|
||
-- Партиции нет, потому что карта пересчитывается целиком и её зерно не
|
||
-- связано с днём.
|
||
CREATE TABLE IF NOT EXISTS dds.identity_map_rep ON CLUSTER clickstream_cluster
|
||
(
|
||
client_id UInt64,
|
||
user_id UInt64,
|
||
_load_id String,
|
||
_load_ts DateTime64(3, 'UTC')
|
||
)
|
||
ENGINE = ReplicatedReplacingMergeTree(
|
||
'/clickhouse/tables/{shard}/{database}/{table}',
|
||
'{replica}',
|
||
_load_ts
|
||
)
|
||
ORDER BY (client_id, user_id);
|
||
|
||
-- Карта лежит по ключу будущего соединения с событиями и сессиями.
|
||
CREATE TABLE IF NOT EXISTS dds.identity_map_dist ON CLUSTER clickstream_cluster
|
||
AS dds.identity_map_rep
|
||
ENGINE = Distributed(
|
||
'clickstream_cluster',
|
||
'dds',
|
||
'identity_map_rep',
|
||
cityHash64(client_id)
|
||
);
|