feat(dm): добавлены витрины выручки и трафика
Зачем: - Показать два ритма пересборки агрегатов на готовых витринах DM. Что: - Добавлены физические модели и публичные договоры revenue_daily и daily_traffic. - Добавлен dm_transform с оконной заменой выручки и полным пересчетом трафика. - Отрицательный опыт локального соединения через публичные _v зафиксирован; выбран явный GLOBAL LEFT JOIN. - Замены партиций сериализованы после подтвержденного OOM при трех одновременных задачах. - Обновлены архитектурные документы и мастер-спека. Проверка: - make lint, make config-test, make smoke, make check-clickhouse, make check-services. - Чистая сборка world_recreate, dds_transform и dm_transform; меры сверены с прямыми агрегатами.
This commit is contained in:
1 parent
c9bcacd63b
commit
da0477f0f1
12 files changed
+436
-25
No files matched your search
@@ -17,6 +17,9 @@ CREATE DATABASE IF NOT EXISTS ods ON CLUSTER clickstream_cluster;
|
||||
-- База DDS заводится здесь же по той же причине, что и ODS.
|
||||
CREATE DATABASE IF NOT EXISTS dds ON CLUSTER clickstream_cluster;
|
||||
|
||||
-- База витрин завершает цепочку STG → ODS → DDS → DM.
|
||||
CREATE DATABASE IF NOT EXISTS dm ON CLUSTER clickstream_cluster;
|
||||
|
||||
-- Справочники стоят вне цепочки STG → ODS → DDS → DM: в хранилище их никто не
|
||||
-- производит, а читают их несколько слоёв — ADR 0012. Порядок слоёв к зоне не
|
||||
-- применяется, её файл идёт сразу за этим.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
-- DM: физические дневные витрины и донор партиций выручки.
|
||||
|
||||
-- Выручка хранится в зерне «день заказа × категория товара». День служит
|
||||
-- единицей пересборки, поэтому совпадает с ключом партиции.
|
||||
CREATE TABLE IF NOT EXISTS dm.revenue_daily_rep ON CLUSTER clickstream_cluster
|
||||
(
|
||||
report_date Date,
|
||||
product_category LowCardinality(String),
|
||||
orders UInt64,
|
||||
units UInt64,
|
||||
revenue Decimal(18, 2),
|
||||
aov Decimal(18, 2),
|
||||
_load_id String,
|
||||
_load_ts DateTime64(3, 'UTC')
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
|
||||
PARTITION BY report_date
|
||||
ORDER BY (report_date, product_category);
|
||||
|
||||
-- Категория — единственное измерение кроме дня. Шесть категорий стартового
|
||||
-- мира близки по объему, поэтому простой ключ раскладывает строки достаточно
|
||||
-- ровно и не добавляет в учебный пример составной хеш без читателя.
|
||||
CREATE TABLE IF NOT EXISTS dm.revenue_daily_dist ON CLUSTER clickstream_cluster
|
||||
AS dm.revenue_daily_rep
|
||||
ENGINE = Distributed(
|
||||
'clickstream_cluster',
|
||||
'dm',
|
||||
'revenue_daily_rep',
|
||||
cityHash64(product_category)
|
||||
);
|
||||
|
||||
-- Донор объявлен через AS и шардируется тем же ключом, что цель. REPLACE
|
||||
-- PARTITION идет по локальным таблицам, поэтому структура и раскладка донора
|
||||
-- обязаны совпадать с целью (docs/architecture/storage.md, «Раскладка по
|
||||
-- шардам»).
|
||||
CREATE TABLE IF NOT EXISTS dm.revenue_daily_stage_rep ON CLUSTER clickstream_cluster
|
||||
AS dm.revenue_daily_rep
|
||||
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
|
||||
PARTITION BY report_date
|
||||
ORDER BY (report_date, product_category);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dm.revenue_daily_stage_dist ON CLUSTER clickstream_cluster
|
||||
AS dm.revenue_daily_stage_rep
|
||||
ENGINE = Distributed(
|
||||
'clickstream_cluster',
|
||||
'dm',
|
||||
'revenue_daily_stage_rep',
|
||||
cityHash64(product_category)
|
||||
);
|
||||
|
||||
-- Трафик пересчитывается целиком: новая связь куки с пользователем меняет
|
||||
-- прошлые дни. Версия запуска позволяет точному представлению скрыть строки
|
||||
-- прежних пересчетов без TRUNCATE и без партиций.
|
||||
CREATE TABLE IF NOT EXISTS dm.daily_traffic_rep ON CLUSTER clickstream_cluster
|
||||
(
|
||||
report_date Date,
|
||||
visitors UInt64,
|
||||
known_users UInt64,
|
||||
_load_id String,
|
||||
_load_ts DateTime64(3, 'UTC')
|
||||
)
|
||||
ENGINE = ReplicatedReplacingMergeTree(
|
||||
'/clickhouse/tables/{shard}/{database}/{table}',
|
||||
'{replica}',
|
||||
_load_ts
|
||||
)
|
||||
ORDER BY report_date;
|
||||
|
||||
-- Все версии одного дня должны попадать на один шард, иначе FINAL не сможет
|
||||
-- выбрать последнюю через границу шардов.
|
||||
CREATE TABLE IF NOT EXISTS dm.daily_traffic_dist ON CLUSTER clickstream_cluster
|
||||
AS dm.daily_traffic_rep
|
||||
ENGINE = Distributed(
|
||||
'clickstream_cluster',
|
||||
'dm',
|
||||
'daily_traffic_rep',
|
||||
cityHash64(report_date)
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
-- DM: публичные договоры стендовых витрин.
|
||||
|
||||
-- День и категория — зерно выручки. Служебные колонки показывают запуск,
|
||||
-- который последним заменил дневную партицию.
|
||||
CREATE VIEW IF NOT EXISTS dm.revenue_daily_v ON CLUSTER clickstream_cluster
|
||||
AS
|
||||
SELECT
|
||||
report_date,
|
||||
product_category,
|
||||
orders,
|
||||
units,
|
||||
revenue,
|
||||
aov,
|
||||
_load_id,
|
||||
_load_ts
|
||||
FROM dm.revenue_daily_dist;
|
||||
|
||||
-- Полный пересчет пишет новую версию каждого дня. FINAL оставляет текущие
|
||||
-- счетчики и прячет повторы запусков от потребителя.
|
||||
CREATE VIEW IF NOT EXISTS dm.daily_traffic_v ON CLUSTER clickstream_cluster
|
||||
AS
|
||||
SELECT
|
||||
report_date,
|
||||
visitors,
|
||||
known_users,
|
||||
_load_id,
|
||||
_load_ts
|
||||
FROM dm.daily_traffic_dist FINAL;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- DM: полный пересчет дневного трафика.
|
||||
--
|
||||
-- Публичные _v скрывают Distributed-таблицы от локальной подстановки:
|
||||
-- EXPLAIN на ClickHouse 26.3 показал чтение обоих шардов на инициаторе.
|
||||
-- Поэтому GLOBAL назван явно, а физические таблицы не подставлены. LEFT JOIN
|
||||
-- сохраняет анонимные сессии.
|
||||
INSERT INTO dm.daily_traffic_dist
|
||||
(
|
||||
report_date,
|
||||
visitors,
|
||||
known_users,
|
||||
_load_id,
|
||||
_load_ts
|
||||
)
|
||||
SELECT
|
||||
sessions.session_date,
|
||||
uniqExact(sessions.client_id),
|
||||
uniqExactIf(identities.user_id, identities.user_id != 0),
|
||||
{load_id:String},
|
||||
now64(3, 'UTC')
|
||||
FROM dds.session_v AS sessions
|
||||
GLOBAL LEFT JOIN dds.identity_map_v AS identities USING (client_id)
|
||||
GROUP BY sessions.session_date
|
||||
-- Ноль оставляет итоговую группировку на инициаторе: иначе каждый шард записал
|
||||
-- бы свою строку дня, а FINAL через границу шардов их не объединяет.
|
||||
SETTINGS distributed_foreground_insert = 1,
|
||||
parallel_distributed_insert_select = 0;
|
||||
@@ -0,0 +1,55 @@
|
||||
-- DM: сборка выручки в донор перед заменой дневных партиций.
|
||||
-- DROP PARTITION ALL очищает локальные таблицы на каждом узле; причина не
|
||||
-- применять TRUNCATE разобрана в соседнем sql/dds/order_rebuild.sql.
|
||||
|
||||
ALTER TABLE dm.revenue_daily_stage_rep ON CLUSTER clickstream_cluster
|
||||
DROP PARTITION ALL;
|
||||
|
||||
INSERT INTO dm.revenue_daily_stage_dist
|
||||
(
|
||||
report_date,
|
||||
product_category,
|
||||
orders,
|
||||
units,
|
||||
revenue,
|
||||
aov,
|
||||
_load_id,
|
||||
_load_ts
|
||||
)
|
||||
SELECT
|
||||
report_date,
|
||||
product_category,
|
||||
orders,
|
||||
units,
|
||||
CAST(revenue, 'Decimal(18, 2)'),
|
||||
-- Обычное деление Decimal усекает доли копейки вместо округления;
|
||||
-- запас масштаба сохраняет их до денежного результата.
|
||||
CAST(
|
||||
round(divideDecimal(revenue, toDecimal128(orders, 0), 6), 2),
|
||||
'Decimal(18, 2)'
|
||||
),
|
||||
{load_id:String},
|
||||
now64(3, 'UTC')
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
order_date AS report_date,
|
||||
dictGet('dic.products', 'category', item_sku) AS product_category,
|
||||
uniqExact(order_id) AS orders,
|
||||
sum(item_quantity) AS units,
|
||||
sum(item_price * item_quantity) AS revenue
|
||||
FROM dds.order_v
|
||||
ARRAY JOIN
|
||||
item_sku,
|
||||
item_quantity,
|
||||
item_price
|
||||
WHERE status = 'paid'
|
||||
AND order_date BETWEEN {first_day:Date} AND {last_day:Date}
|
||||
GROUP BY
|
||||
report_date,
|
||||
product_category
|
||||
)
|
||||
-- Источник лежит по заказу, цель — по категории. Ноль заставляет инициатор
|
||||
-- слить частичные агрегаты и разложить готовые строки по ключу цели.
|
||||
SETTINGS distributed_foreground_insert = 1,
|
||||
parallel_distributed_insert_select = 0;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- DM: подмена одного дня выручки готовой партицией донора.
|
||||
--
|
||||
-- День приходит из результата revenue_daily_scope.sql, а не от пользователя.
|
||||
-- Jinja нужна потому, что ClickHouse не подставляет параметр в PARTITION.
|
||||
-- Замена идет на локальной таблице ON CLUSTER: граница атомарности и повтор
|
||||
-- разобраны на таком же шаге в sql/dds/order_replace.sql.
|
||||
ALTER TABLE dm.revenue_daily_rep ON CLUSTER clickstream_cluster
|
||||
REPLACE PARTITION '{{ params.day }}' FROM dm.revenue_daily_stage_rep;
|
||||
@@ -0,0 +1,35 @@
|
||||
-- DM: объем пересборки выручки.
|
||||
--
|
||||
-- Последний слепок ODS называет дни, состояние которых источник еще может
|
||||
-- изменить. Сам агрегат читает только DDS; ODS здесь нужен лишь для границ.
|
||||
-- Первый запуск и повтор после частичного отказа расширяют левую границу до
|
||||
-- первого дня оплаченных заказов, которого еще нет в витрине.
|
||||
--
|
||||
-- Файл кончается без точки с запятой: даг читает его одним запросом и ждет
|
||||
-- одну строку с границами.
|
||||
WITH
|
||||
(SELECT max(snapshot_date) FROM ods.order_dist) AS last_snapshot,
|
||||
(
|
||||
SELECT min(toDate(toTimeZone(created_at, 'Europe/Samara')))
|
||||
FROM ods.order_dist
|
||||
WHERE snapshot_date = last_snapshot
|
||||
) AS window_start,
|
||||
(
|
||||
SELECT max(toDate(toTimeZone(created_at, 'Europe/Samara')))
|
||||
FROM ods.order_dist
|
||||
WHERE snapshot_date = last_snapshot
|
||||
) AS window_end,
|
||||
(
|
||||
SELECT minOrNull(report_date)
|
||||
FROM (SELECT DISTINCT order_date AS report_date FROM dds.order_v WHERE status = 'paid')
|
||||
-- Правая сторона тоже распределенная: GLOBAL собирает множество дней
|
||||
-- на инициаторе и не превращает запрос в запрещенное двойное чтение.
|
||||
WHERE report_date GLOBAL NOT IN (
|
||||
SELECT report_date FROM dm.revenue_daily_dist
|
||||
)
|
||||
) AS first_missing_day
|
||||
SELECT
|
||||
least(window_start, coalesce(first_missing_day, window_start)) AS first_day,
|
||||
window_end AS last_day
|
||||
WHERE (SELECT count() FROM dds.order_v) > 0
|
||||
AND (SELECT count() FROM ods.order_dist) > 0
|
||||
Reference in new issue
Block a user