feat(dm): добавлена сверка покупок с заказами
- Зачем: - показать расхождения трекера и бэкенда на зерне заказа - сохранить отложенные покупки наблюдаемыми до следующего слепка - Что: - добавлены таблицы, представление и пересборка purchase_vs_orders - сумма покупки нормализована в Decimal на границе ODS - обновлены договоры и указатели на механику пересборки - Проверка: - `make config-test`, `make lint` - `cd generator && make docs lint typecheck test` - `make rebuild-storage`, явные `dds_transform` и `dm_transform` - `make smoke`, `make check-clickhouse`, `make check-services`
This commit is contained in:
1 parent
fb00bbe4c3
commit
20071c565f
15 files changed
+391
-38
No files matched your search
@@ -0,0 +1,71 @@
|
||||
-- DM: сверка покупок трекера с текущими заказами бэкенда.
|
||||
--
|
||||
-- Обе стороны FULL OUTER JOIN — подзапросы. С голой левой Distributed
|
||||
-- ClickHouse вернул бы непарный заказ по разу с каждого шарда; форма измерена
|
||||
-- в docs/research/2026-08-22-distributed-join.md.
|
||||
-- Выбор DROP PARTITION ALL вместо TRUNCATE разобран в соседнем
|
||||
-- sql/dds/order_rebuild.sql.
|
||||
|
||||
ALTER TABLE dm.purchase_vs_orders_stage_rep ON CLUSTER clickstream_cluster
|
||||
DROP PARTITION ALL;
|
||||
|
||||
INSERT INTO dm.purchase_vs_orders_stage_dist
|
||||
(
|
||||
order_day,
|
||||
order_id,
|
||||
declared_revenue,
|
||||
items_total,
|
||||
status,
|
||||
mismatch_class,
|
||||
_load_id,
|
||||
_load_ts
|
||||
)
|
||||
SELECT
|
||||
-- У парного заказа день берется у бэкенда; трекер дает его только
|
||||
-- непарной покупке.
|
||||
coalesce(order_date, purchase_day),
|
||||
coalesce(purchase_order_id, backend_order_id),
|
||||
declared_revenue,
|
||||
items_total,
|
||||
status,
|
||||
multiIf(
|
||||
backend_order_id IS NULL, 'awaiting_order',
|
||||
status = 'cancelled', 'cancelled',
|
||||
purchase_order_id IS NULL, 'lost_event',
|
||||
purchase_count > 1, 'duplicate_event',
|
||||
declared_revenue != items_total, 'amount_delta',
|
||||
'match'
|
||||
),
|
||||
{load_id:String},
|
||||
now64(3, 'UTC')
|
||||
FROM
|
||||
(
|
||||
-- Контракт purchase несет один номер и одну объявленную сумму в массивах.
|
||||
-- Бизнес-дубль повторяет оба значения, поэтому count хранится отдельно,
|
||||
-- а сумма берется одна — складывать дубли значило бы выдумать дельту.
|
||||
SELECT
|
||||
purchase_id[1] AS purchase_order_id,
|
||||
any(event_date) AS purchase_day,
|
||||
any(purchase_revenue[1]) AS declared_revenue,
|
||||
count() AS purchase_count
|
||||
FROM dds.event_v
|
||||
WHERE event_type = 'purchase'
|
||||
AND event_date BETWEEN {first_day:Date} AND {last_day:Date}
|
||||
GROUP BY purchase_order_id
|
||||
) AS purchases
|
||||
GLOBAL FULL OUTER JOIN
|
||||
(
|
||||
SELECT
|
||||
order_id AS backend_order_id,
|
||||
order_date,
|
||||
items_total,
|
||||
status
|
||||
FROM dds.order_v
|
||||
WHERE order_date BETWEEN {first_day:Date} AND {last_day:Date}
|
||||
) AS orders ON purchase_order_id = backend_order_id
|
||||
-- NULL обозначает отсутствующую сторону. При join_use_nulls = 0 ClickHouse
|
||||
-- заполнил бы ее нулем или пустой строкой и скрыл отсутствие источника.
|
||||
-- Вставка идет через ключ цели: источники разложены по другим ключам.
|
||||
SETTINGS join_use_nulls = 1,
|
||||
distributed_foreground_insert = 1,
|
||||
parallel_distributed_insert_select = 0;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- DM: подмена одного дня сверки собранной партицией донора.
|
||||
--
|
||||
-- День для PARTITION берет purchase_vs_orders_scope.sql. Выбор Jinja,
|
||||
-- атомарность и повтор разобраны в sql/dds/order_replace.sql.
|
||||
--
|
||||
-- Пустая партиция законна: если источник больше не показывает строк дня,
|
||||
-- замена убирает этот день из витрины.
|
||||
ALTER TABLE dm.purchase_vs_orders_rep ON CLUSTER clickstream_cluster
|
||||
REPLACE PARTITION '{{ params.day }}' FROM dm.purchase_vs_orders_stage_rep;
|
||||
@@ -0,0 +1,62 @@
|
||||
-- DM: объем пересборки сверки покупок с заказами.
|
||||
--
|
||||
-- У источников разные правые границы. Заказы называют изменяемое окно ярлыком
|
||||
-- последнего принятого слепка. События заканчиваются последним прожитым днем,
|
||||
-- который выводится из позиции мира. Одна граница другую не подменяет.
|
||||
--
|
||||
-- Левая граница возвращает не только пропуски, но и самый ранний
|
||||
-- awaiting_order. Такой день уже есть в витрине, поэтому обычный поиск дыры
|
||||
-- его не увидит. Иначе пакетный разгон длиннее K навсегда оставил бы прежний
|
||||
-- хвост предварительным.
|
||||
--
|
||||
-- Файл заканчивается без точки с запятой: почему — sql/dds/order_scope.sql.
|
||||
WITH
|
||||
(SELECT min(event_date) FROM dds.event_v) AS first_event_day,
|
||||
first_event_day + ({position:UInt16} - 1) AS last_lived_day,
|
||||
(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 maxOrNull(event_date)
|
||||
FROM dds.event_v
|
||||
WHERE event_type = 'purchase' AND event_date <= last_lived_day
|
||||
) AS last_purchase_day,
|
||||
(
|
||||
SELECT minOrNull(day)
|
||||
FROM (
|
||||
SELECT order_date AS day
|
||||
FROM dds.order_v
|
||||
UNION DISTINCT
|
||||
SELECT event_date AS day
|
||||
FROM dds.event_v
|
||||
WHERE event_type = 'purchase' AND event_date <= last_lived_day
|
||||
)
|
||||
WHERE day GLOBAL NOT IN (
|
||||
SELECT order_day FROM dm.purchase_vs_orders_dist
|
||||
)
|
||||
) AS first_missing_day,
|
||||
(
|
||||
SELECT minOrNull(order_day)
|
||||
FROM dm.purchase_vs_orders_dist
|
||||
WHERE mismatch_class = 'awaiting_order'
|
||||
) AS first_provisional_day
|
||||
SELECT
|
||||
least(
|
||||
window_start,
|
||||
coalesce(first_missing_day, window_start),
|
||||
coalesce(first_provisional_day, window_start)
|
||||
) AS first_day,
|
||||
greatest(window_end, last_purchase_day) AS last_day
|
||||
-- Без принятого слепка неизвестно окно заказов. Без готового DDS или прожитой
|
||||
-- покупки донор был бы пуст, поэтому подменять партиции еще нельзя.
|
||||
WHERE (SELECT count() FROM ods.order_dist) > 0
|
||||
AND (SELECT count() FROM dds.order_v) > 0
|
||||
AND last_purchase_day IS NOT NULL
|
||||
Reference in new issue
Block a user