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:
ddadmin committed 2026-08-26 13:55:26 -04:00
1 parent c9bcacd63b
commit da0477f0f1
12 files changed
+436 -25

No files matched your search

+27
View File
@@ -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;
+55
View File
@@ -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;
+8
View File
@@ -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;
+35
View File
@@ -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