feat(dq): добавлен контроль качества конвейера
- Зачем: - конвейеру нужна публикуемая сводка текущего качества с красным завершением при нарушении договоров. - Что: - добавлены DDL, публичное представление и пять сверок на естественном деловом зерне. - даг dq_check встроен после DM и публикует полные дневные партиции до именованных утверждений. - параллелизм DDS, DM и DQ приведён к измеренному лимиту памяти планировщика. - Проверка: - make lint; make config-test; make rebuild-storage. - make smoke; make check-clickhouse; make check-services. - три семейства проверок покрашены искусственными расхождениями и восстановлены повторным полным прогоном.
This commit is contained in:
1 parent
4e60aafc31
commit
26b2e5fea1
17 files changed
+753
-38
No files matched your search
@@ -121,3 +121,50 @@ ENGINE = Distributed(
|
||||
'daily_traffic_rep',
|
||||
cityHash64(report_date)
|
||||
);
|
||||
|
||||
-- Сводка качества хранит текущее состояние атомарной проверки одного дня.
|
||||
-- История запусков остаётся в Airflow, поэтому версия строки не нужна:
|
||||
-- следующий успешный прогон заменяет дневную партицию целиком.
|
||||
CREATE TABLE IF NOT EXISTS dm.dq_summary_rep ON CLUSTER clickstream_cluster
|
||||
(
|
||||
data_date Date,
|
||||
check_name LowCardinality(String),
|
||||
reference_rows UInt64,
|
||||
actual_rows UInt64,
|
||||
failed_rows UInt64,
|
||||
status LowCardinality(String),
|
||||
_load_id String,
|
||||
_load_ts DateTime64(3, 'UTC')
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
|
||||
PARTITION BY data_date
|
||||
ORDER BY (data_date, check_name);
|
||||
|
||||
-- Все строки одной проверки лежат на одном шарде. Таблица мала, а ключ
|
||||
-- check_name делает раскладку донора и цели одинаковой без нового измерения.
|
||||
CREATE TABLE IF NOT EXISTS dm.dq_summary_dist ON CLUSTER clickstream_cluster
|
||||
AS dm.dq_summary_rep
|
||||
ENGINE = Distributed(
|
||||
'clickstream_cluster',
|
||||
'dm',
|
||||
'dq_summary_rep',
|
||||
cityHash64(check_name)
|
||||
);
|
||||
|
||||
-- Источник и цель REPLACE PARTITION обязаны совпадать по структуре, ключам и
|
||||
-- политике хранения. Требование повторно сверено по документации ClickHouse
|
||||
-- через Context7 27 августа 2026 года; AS не даёт формам разойтись.
|
||||
CREATE TABLE IF NOT EXISTS dm.dq_summary_stage_rep ON CLUSTER clickstream_cluster
|
||||
AS dm.dq_summary_rep
|
||||
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
|
||||
PARTITION BY data_date
|
||||
ORDER BY (data_date, check_name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dm.dq_summary_stage_dist ON CLUSTER clickstream_cluster
|
||||
AS dm.dq_summary_stage_rep
|
||||
ENGINE = Distributed(
|
||||
'clickstream_cluster',
|
||||
'dm',
|
||||
'dq_summary_stage_rep',
|
||||
cityHash64(check_name)
|
||||
);
|
||||
@@ -42,3 +42,18 @@ SELECT
|
||||
_load_id,
|
||||
_load_ts
|
||||
FROM dm.daily_traffic_dist FINAL;
|
||||
|
||||
-- Сводка отвечает, какие атомарные договоры дня выполняются сейчас.
|
||||
-- Явный список не пропускает в публичный договор будущие физические колонки.
|
||||
CREATE VIEW IF NOT EXISTS dm.dq_summary_v ON CLUSTER clickstream_cluster
|
||||
AS
|
||||
SELECT
|
||||
data_date,
|
||||
check_name,
|
||||
reference_rows,
|
||||
actual_rows,
|
||||
failed_rows,
|
||||
status,
|
||||
_load_id,
|
||||
_load_ts
|
||||
FROM dm.dq_summary_dist;
|
||||
@@ -0,0 +1,17 @@
|
||||
-- DQ: стендовая сторона технической растяжки от описи мира.
|
||||
--
|
||||
-- Эталонные счётчики читает Airflow из world-inventory.json: у настоящего
|
||||
-- магазина такого оракула нет. Запрос возвращает только наблюдаемую сторону;
|
||||
-- Python-задача сопоставляет ее с тремя счетчиками закрытого дня из описи.
|
||||
SELECT
|
||||
order_day AS data_date,
|
||||
mismatch_class,
|
||||
count() AS orders
|
||||
FROM dm.purchase_vs_orders_v
|
||||
WHERE mismatch_class IN ('match', 'cancelled', 'amount_delta')
|
||||
GROUP BY
|
||||
data_date,
|
||||
mismatch_class
|
||||
ORDER BY
|
||||
data_date,
|
||||
mismatch_class
|
||||
@@ -0,0 +1,46 @@
|
||||
-- DQ: дневной трафик против прямого агрегата сессий и карты идентичностей.
|
||||
--
|
||||
-- Публичные представления скрывают физическую ко-локацию, поэтому GLOBAL
|
||||
-- назван явно, как в самой витрине. Полное объединение не прячет день,
|
||||
-- исчезнувший с любой стороны.
|
||||
WITH
|
||||
reference AS
|
||||
(
|
||||
SELECT
|
||||
sessions.session_date AS report_date,
|
||||
uniqExact(sessions.client_id) AS visitors,
|
||||
uniqExactIf(identities.user_id, identities.user_id != 0) AS known_users
|
||||
FROM dds.session_v AS sessions
|
||||
GLOBAL LEFT JOIN dds.identity_map_v AS identities USING (client_id)
|
||||
GROUP BY report_date
|
||||
),
|
||||
actual AS
|
||||
(
|
||||
SELECT
|
||||
report_date,
|
||||
count() AS physical_rows,
|
||||
any(visitors) AS visitors,
|
||||
any(known_users) AS known_users
|
||||
FROM dm.daily_traffic_v
|
||||
GROUP BY report_date
|
||||
)
|
||||
SELECT
|
||||
coalesce(reference.report_date, actual.report_date) AS data_date,
|
||||
toString(coalesce(reference.report_date, actual.report_date)) AS business_key,
|
||||
isNotNull(reference.report_date) AS reference_present,
|
||||
isNotNull(actual.report_date) AS actual_present,
|
||||
toString(tuple(reference.visitors, reference.known_users)) AS reference_value,
|
||||
concat(
|
||||
toString(tuple(actual.visitors, actual.known_users)),
|
||||
', rows=',
|
||||
toString(actual.physical_rows)
|
||||
) AS actual_value,
|
||||
isNull(reference.report_date)
|
||||
OR isNull(actual.report_date)
|
||||
OR actual.physical_rows != 1
|
||||
OR NOT isNotDistinctFrom(reference.visitors, actual.visitors)
|
||||
OR NOT isNotDistinctFrom(reference.known_users, actual.known_users) AS failed
|
||||
FROM reference
|
||||
GLOBAL FULL OUTER JOIN actual USING (report_date)
|
||||
ORDER BY data_date
|
||||
SETTINGS join_use_nulls = 1
|
||||
@@ -0,0 +1,7 @@
|
||||
-- DQ: общий донор очищается перед сборкой всех проверок.
|
||||
--
|
||||
-- Если одна сборка упадет, публикация не начнется. Следующий запуск удалит
|
||||
-- неполный результат здесь и соберет донор заново.
|
||||
|
||||
ALTER TABLE dm.dq_summary_stage_rep ON CLUSTER clickstream_cluster
|
||||
DROP PARTITION ALL;
|
||||
@@ -0,0 +1,95 @@
|
||||
-- DQ: строка сверки покупок и заказов против независимой сборки из DDS.
|
||||
--
|
||||
-- Проверяются все деловые поля. Счёт физических строк отдельно ловит дубль
|
||||
-- ключа, даже если обе копии несут одинаковые значения.
|
||||
WITH
|
||||
purchases AS
|
||||
(
|
||||
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'
|
||||
GROUP BY purchase_order_id
|
||||
),
|
||||
orders AS
|
||||
(
|
||||
SELECT
|
||||
order_id AS backend_order_id,
|
||||
order_date,
|
||||
items_total,
|
||||
status
|
||||
FROM dds.order_v
|
||||
),
|
||||
reference AS
|
||||
(
|
||||
SELECT
|
||||
coalesce(order_date, purchase_day) AS order_day,
|
||||
coalesce(purchase_order_id, backend_order_id) AS 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'
|
||||
) AS mismatch_class
|
||||
FROM purchases
|
||||
GLOBAL FULL OUTER JOIN orders
|
||||
ON purchase_order_id = backend_order_id
|
||||
),
|
||||
actual AS
|
||||
(
|
||||
SELECT
|
||||
order_day,
|
||||
order_id,
|
||||
count() AS physical_rows,
|
||||
any(declared_revenue) AS declared_revenue,
|
||||
any(items_total) AS items_total,
|
||||
any(status) AS status,
|
||||
any(mismatch_class) AS mismatch_class
|
||||
FROM dm.purchase_vs_orders_v
|
||||
GROUP BY
|
||||
order_day,
|
||||
order_id
|
||||
)
|
||||
SELECT
|
||||
coalesce(reference.order_day, actual.order_day) AS data_date,
|
||||
coalesce(reference.order_id, actual.order_id) AS business_key,
|
||||
isNotNull(reference.order_id) AS reference_present,
|
||||
isNotNull(actual.order_id) AS actual_present,
|
||||
toString(tuple(
|
||||
reference.declared_revenue,
|
||||
reference.items_total,
|
||||
reference.status,
|
||||
reference.mismatch_class
|
||||
)) AS reference_value,
|
||||
concat(
|
||||
toString(tuple(
|
||||
actual.declared_revenue,
|
||||
actual.items_total,
|
||||
actual.status,
|
||||
actual.mismatch_class
|
||||
)),
|
||||
', rows=',
|
||||
toString(actual.physical_rows)
|
||||
) AS actual_value,
|
||||
isNull(reference.order_id)
|
||||
OR isNull(actual.order_id)
|
||||
OR actual.physical_rows != 1
|
||||
OR NOT isNotDistinctFrom(reference.declared_revenue, actual.declared_revenue)
|
||||
OR NOT isNotDistinctFrom(reference.items_total, actual.items_total)
|
||||
OR NOT isNotDistinctFrom(reference.status, actual.status)
|
||||
OR NOT isNotDistinctFrom(reference.mismatch_class, actual.mismatch_class)
|
||||
AS failed
|
||||
FROM reference
|
||||
GLOBAL FULL OUTER JOIN actual USING (order_day, order_id)
|
||||
ORDER BY
|
||||
data_date,
|
||||
business_key
|
||||
SETTINGS join_use_nulls = 1
|
||||
@@ -0,0 +1,7 @@
|
||||
-- DQ: публикация одного полностью собранного дня общей сводки.
|
||||
--
|
||||
-- День приходит из самого донора после успеха всех пяти сборок. REPLACE
|
||||
-- PARTITION работает с локальными таблицами одинаковой структуры; это
|
||||
-- ограничение сверено по документации ClickHouse 27 августа 2026 года.
|
||||
ALTER TABLE dm.dq_summary_rep ON CLUSTER clickstream_cluster
|
||||
REPLACE PARTITION '{{ params.day }}' FROM dm.dq_summary_stage_rep;
|
||||
@@ -0,0 +1,77 @@
|
||||
-- DQ: дневная выручка против прямого агрегата заказов DDS.
|
||||
--
|
||||
-- День берётся из объединения сторон. Поэтому исчезнувший целиком день или
|
||||
-- категория остаются видимы. Денежный расчет повторяет публичный договор
|
||||
-- витрины и сохраняет Decimal до округления AOV.
|
||||
WITH
|
||||
reference AS
|
||||
(
|
||||
SELECT
|
||||
report_date,
|
||||
product_category,
|
||||
orders,
|
||||
units,
|
||||
CAST(revenue, 'Decimal(18, 2)') AS revenue,
|
||||
CAST(
|
||||
round(divideDecimal(revenue, toDecimal128(orders, 0), 6), 2),
|
||||
'Decimal(18, 2)'
|
||||
) AS aov
|
||||
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'
|
||||
GROUP BY
|
||||
report_date,
|
||||
product_category
|
||||
)
|
||||
),
|
||||
actual AS
|
||||
(
|
||||
SELECT
|
||||
report_date,
|
||||
product_category,
|
||||
count() AS physical_rows,
|
||||
any(orders) AS orders,
|
||||
any(units) AS units,
|
||||
any(revenue) AS revenue,
|
||||
any(aov) AS aov
|
||||
FROM dm.revenue_daily_v
|
||||
GROUP BY
|
||||
report_date,
|
||||
product_category
|
||||
)
|
||||
SELECT
|
||||
coalesce(reference.report_date, actual.report_date) AS data_date,
|
||||
coalesce(reference.product_category, actual.product_category) AS business_key,
|
||||
isNotNull(reference.product_category) AS reference_present,
|
||||
isNotNull(actual.product_category) AS actual_present,
|
||||
toString(tuple(reference.orders, reference.units, reference.revenue, reference.aov))
|
||||
AS reference_value,
|
||||
concat(
|
||||
toString(tuple(actual.orders, actual.units, actual.revenue, actual.aov)),
|
||||
', rows=',
|
||||
toString(actual.physical_rows)
|
||||
) AS actual_value,
|
||||
isNull(reference.product_category)
|
||||
OR isNull(actual.product_category)
|
||||
OR actual.physical_rows != 1
|
||||
OR NOT isNotDistinctFrom(reference.orders, actual.orders)
|
||||
OR NOT isNotDistinctFrom(reference.units, actual.units)
|
||||
OR NOT isNotDistinctFrom(reference.revenue, actual.revenue)
|
||||
OR NOT isNotDistinctFrom(reference.aov, actual.aov) AS failed
|
||||
FROM reference
|
||||
GLOBAL FULL OUTER JOIN actual USING (report_date, product_category)
|
||||
ORDER BY
|
||||
data_date,
|
||||
business_key
|
||||
SETTINGS join_use_nulls = 1
|
||||
@@ -0,0 +1,45 @@
|
||||
-- DQ: эталонная сессия Метрики против сессии, нарезанной слоем DDS.
|
||||
--
|
||||
-- Строка результата — одна кука одного завершённого дня. Сравнение по ключу
|
||||
-- не даёт лишней сессии одной куки скрыть потерянную сессию другой.
|
||||
WITH
|
||||
(SELECT min(EventDate) FROM ods.event_v) AS first_source_day,
|
||||
first_source_day + ({position:UInt16} - 1) AS last_lived_day,
|
||||
reference AS
|
||||
(
|
||||
SELECT
|
||||
EventDate AS data_date,
|
||||
ClientID AS client_id,
|
||||
uniqExact(VisitID) AS sessions
|
||||
FROM ods.event_v
|
||||
WHERE EventDate <= last_lived_day
|
||||
GROUP BY
|
||||
data_date,
|
||||
client_id
|
||||
),
|
||||
actual AS
|
||||
(
|
||||
SELECT
|
||||
session_date AS data_date,
|
||||
client_id,
|
||||
count() AS sessions
|
||||
FROM dds.session_v
|
||||
WHERE session_date <= last_lived_day
|
||||
GROUP BY
|
||||
data_date,
|
||||
client_id
|
||||
)
|
||||
SELECT
|
||||
coalesce(reference.data_date, actual.data_date) AS data_date,
|
||||
toString(coalesce(reference.client_id, actual.client_id)) AS business_key,
|
||||
isNotNull(reference.client_id) AS reference_present,
|
||||
isNotNull(actual.client_id) AS actual_present,
|
||||
toString(reference.sessions) AS reference_value,
|
||||
toString(actual.sessions) AS actual_value,
|
||||
NOT isNotDistinctFrom(reference.sessions, actual.sessions) AS failed
|
||||
FROM reference
|
||||
GLOBAL FULL OUTER JOIN actual USING (data_date, client_id)
|
||||
ORDER BY
|
||||
data_date,
|
||||
business_key
|
||||
SETTINGS join_use_nulls = 1
|
||||
Reference in new issue
Block a user