petstore-backend/db/queries/pilot_metrics.sql

375 lines
16 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Petstore Pilot Metrics v2MySQL 5.7+/8.x只读
-- 默认时间窗为最近 30 天,但默认门店集合为 0不返回业务数据
-- 执行人必须把 @pilot_store_ids 显式改为 35 个试点门店内部 ID例如 '101,205,309'。
-- 输出仅含 store_id、计数、比率和时效不含客户/员工 PII。
SET @pilot_to = DATE_ADD(CURRENT_DATE, INTERVAL 1 DAY);
SET @pilot_from = DATE_SUB(@pilot_to, INTERVAL 30 DAY);
SET @pilot_store_ids = '0';
-- 1. 核心服务与报告漏斗。[from, to),各事实按稳定 aggregate_id 去重。
-- 比率只认同窗且时间顺序正确的 cohort避免跨窗口事件把比率推到 100% 以上。
SELECT
e.store_id,
COUNT(DISTINCT CASE WHEN e.event_type = 'appointment_created' THEN e.aggregate_id END) AS appointment_created_count,
COUNT(DISTINCT CASE WHEN e.event_type = 'service_started' THEN e.aggregate_id END) AS service_started_count,
COUNT(DISTINCT CASE WHEN e.event_type = 'service_completed' THEN e.aggregate_id END) AS complete_service_count,
COUNT(DISTINCT CASE WHEN e.event_type = 'report_submitted' THEN e.aggregate_id END) AS report_submitted_count,
COUNT(DISTINCT CASE
WHEN e.event_type = 'report_sent'
AND EXISTS (
SELECT 1 FROM t_business_event submitted
WHERE submitted.store_id = e.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = e.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END) AS report_sent_count,
COUNT(DISTINCT CASE
WHEN e.event_type IN ('report_opened', 'report_reopened')
AND EXISTS (
SELECT 1
FROM t_business_event sent
JOIN t_business_event submitted
ON submitted.store_id = sent.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = sent.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= sent.occurred_at
WHERE sent.store_id = e.store_id
AND sent.event_type = 'report_sent'
AND sent.aggregate_type = 'report'
AND sent.aggregate_id = e.aggregate_id
AND sent.occurred_at >= @pilot_from
AND sent.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END) AS report_opened_count,
COUNT(DISTINCT CASE WHEN e.event_type = 'lead_submitted' THEN e.aggregate_id END) AS lead_submitted_count,
COUNT(DISTINCT CASE WHEN e.event_type = 'rebook_created' THEN e.aggregate_id END) AS rebook_created_count,
ROUND(
100 * COUNT(DISTINCT CASE
WHEN e.event_type = 'service_completed'
AND EXISTS (
SELECT 1 FROM t_business_event started
WHERE started.store_id = e.store_id
AND started.event_type = 'service_started'
AND started.aggregate_type = 'appointment'
AND started.aggregate_id = e.aggregate_id
AND started.occurred_at >= @pilot_from
AND started.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END)
/ NULLIF(COUNT(DISTINCT CASE WHEN e.event_type = 'service_started' THEN e.aggregate_id END), 0),
1
) AS service_completion_rate_pct,
ROUND(
100 * COUNT(DISTINCT CASE
WHEN e.event_type = 'report_sent'
AND EXISTS (
SELECT 1 FROM t_business_event submitted
WHERE submitted.store_id = e.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = e.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END)
/ NULLIF(COUNT(DISTINCT CASE WHEN e.event_type = 'report_submitted' THEN e.aggregate_id END), 0),
1
) AS report_sent_rate_pct,
ROUND(
100 * COUNT(DISTINCT CASE
WHEN e.event_type IN ('report_opened', 'report_reopened')
AND EXISTS (
SELECT 1
FROM t_business_event sent
JOIN t_business_event submitted
ON submitted.store_id = sent.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = sent.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= sent.occurred_at
WHERE sent.store_id = e.store_id
AND sent.event_type = 'report_sent'
AND sent.aggregate_type = 'report'
AND sent.aggregate_id = e.aggregate_id
AND sent.occurred_at >= @pilot_from
AND sent.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END)
/ NULLIF(COUNT(DISTINCT CASE
WHEN e.event_type = 'report_sent'
AND EXISTS (
SELECT 1 FROM t_business_event submitted
WHERE submitted.store_id = e.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = e.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END), 0),
1
) AS report_open_rate_pct,
ROUND(
100 * COUNT(DISTINCT CASE
WHEN e.event_type = 'lead_submitted'
AND lead_fact.report_id IS NOT NULL
AND EXISTS (
SELECT 1
FROM t_business_event opened
JOIN t_business_event sent
ON sent.store_id = opened.store_id
AND sent.event_type = 'report_sent'
AND sent.aggregate_type = 'report'
AND sent.aggregate_id = opened.aggregate_id
AND sent.occurred_at >= @pilot_from
AND sent.occurred_at <= opened.occurred_at
JOIN t_business_event submitted
ON submitted.store_id = sent.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = sent.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= sent.occurred_at
WHERE opened.store_id = e.store_id
AND opened.event_type IN ('report_opened', 'report_reopened')
AND opened.aggregate_type = 'report'
AND opened.aggregate_id = lead_fact.report_id
AND opened.occurred_at >= @pilot_from
AND opened.occurred_at <= e.occurred_at
)
THEN lead_fact.report_id
END)
/ NULLIF(COUNT(DISTINCT CASE
WHEN e.event_type IN ('report_opened', 'report_reopened')
AND EXISTS (
SELECT 1
FROM t_business_event sent
JOIN t_business_event submitted
ON submitted.store_id = sent.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = sent.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= sent.occurred_at
WHERE sent.store_id = e.store_id
AND sent.event_type = 'report_sent'
AND sent.aggregate_type = 'report'
AND sent.aggregate_id = e.aggregate_id
AND sent.occurred_at >= @pilot_from
AND sent.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END), 0),
1
) AS opened_to_lead_rate_pct,
ROUND(
100 * COUNT(DISTINCT CASE
WHEN e.event_type = 'rebook_created'
AND rebook.source_lead_id IS NOT NULL
AND EXISTS (
SELECT 1 FROM t_business_event submitted_lead
WHERE submitted_lead.store_id = e.store_id
AND submitted_lead.event_type = 'lead_submitted'
AND submitted_lead.aggregate_type = 'report_lead'
AND submitted_lead.aggregate_id = rebook.source_lead_id
AND submitted_lead.occurred_at >= @pilot_from
AND submitted_lead.occurred_at <= e.occurred_at
)
THEN rebook.source_lead_id
END)
/ NULLIF(COUNT(DISTINCT CASE WHEN e.event_type = 'lead_submitted' THEN e.aggregate_id END), 0),
1
) AS lead_to_rebook_rate_pct
FROM t_business_event e
LEFT JOIN t_report_lead lead_fact
ON e.event_type = 'lead_submitted'
AND lead_fact.id = e.aggregate_id
AND lead_fact.store_id = e.store_id
LEFT JOIN t_follow_up_task rebook
ON e.event_type = 'rebook_created'
AND rebook.rebooked_appointment_id = e.aggregate_id
AND rebook.store_id = e.store_id
WHERE e.occurred_at >= @pilot_from
AND e.occurred_at < @pilot_to
AND FIND_IN_SET(CAST(e.store_id AS CHAR), @pilot_store_ids) > 0
GROUP BY e.store_id
ORDER BY e.store_id;
-- 2. 报告发送时效:只纳入已经完整观察 24 小时的提交 cohort。
SELECT
submitted.store_id,
COUNT(*) AS matured_submitted_report_count,
SUM(CASE
WHEN sent.occurred_at IS NOT NULL
AND sent.occurred_at >= submitted.occurred_at
AND sent.occurred_at <= DATE_ADD(submitted.occurred_at, INTERVAL 24 HOUR)
THEN 1 ELSE 0
END) AS sent_within_24h_count,
ROUND(
100 * SUM(CASE
WHEN sent.occurred_at IS NOT NULL
AND sent.occurred_at >= submitted.occurred_at
AND sent.occurred_at <= DATE_ADD(submitted.occurred_at, INTERVAL 24 HOUR)
THEN 1 ELSE 0
END) / NULLIF(COUNT(*), 0),
1
) AS sent_within_24h_rate_pct
FROM t_business_event submitted
LEFT JOIN t_business_event sent
ON sent.store_id = submitted.store_id
AND sent.event_type = 'report_sent'
AND sent.aggregate_type = 'report'
AND sent.aggregate_id = submitted.aggregate_id
WHERE submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at < DATE_SUB(@pilot_to, INTERVAL 24 HOUR)
AND FIND_IN_SET(CAST(submitted.store_id AS CHAR), @pilot_store_ids) > 0
GROUP BY submitted.store_id
ORDER BY submitted.store_id;
-- 3. 回访执行质量:以任务 due_date 落在窗口内为队列口径;退订不计员工完成。
SELECT
t.store_id,
COUNT(*) AS due_follow_up_count,
SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) AS staff_completed_count,
SUM(CASE WHEN t.status = 'canceled' AND t.outcome = 'unsubscribed' THEN 1 ELSE 0 END) AS unsubscribed_count,
SUM(CASE WHEN t.status IN ('pending', 'in_progress') THEN 1 ELSE 0 END) AS still_open_count,
SUM(CASE
WHEN t.status IN ('pending', 'in_progress') AND t.due_date < CURRENT_DATE
THEN 1 ELSE 0
END) AS overdue_open_count,
SUM(CASE
WHEN t.status = 'completed'
AND t.closed_at < DATE_ADD(t.due_date, INTERVAL 3 DAY)
THEN 1 ELSE 0
END) AS completed_by_due_plus_2d_count,
ROUND(
100 * SUM(CASE
WHEN t.status = 'completed'
AND t.closed_at < DATE_ADD(t.due_date, INTERVAL 3 DAY)
THEN 1 ELSE 0
END) / NULLIF(SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END), 0),
1
) AS on_time_completion_rate_pct,
ROUND(AVG(t.attempt_count), 2) AS average_attempt_count,
SUM(CASE WHEN t.outcome = 'rebooked' THEN 1 ELSE 0 END) AS rebooked_task_count,
SUM(CASE WHEN t.outcome = 'not_interested' THEN 1 ELSE 0 END) AS not_interested_count,
SUM(CASE WHEN t.outcome = 'invalid_contact' THEN 1 ELSE 0 END) AS invalid_contact_count
FROM t_follow_up_task t
WHERE t.due_date >= DATE(@pilot_from)
AND t.due_date < DATE(@pilot_to)
AND FIND_IN_SET(CAST(t.store_id AS CHAR), @pilot_store_ids) > 0
GROUP BY t.store_id
ORDER BY t.store_id;
-- 4. 约 200 次完整服务样本闸门:严格使用试点窗口,按店 + 最后一行试点店合计。
SELECT
e.store_id,
COUNT(DISTINCT e.aggregate_id) AS complete_service_count,
MIN(e.occurred_at) AS first_complete_service_at,
MAX(e.occurred_at) AS latest_complete_service_at
FROM t_business_event e
WHERE e.event_type = 'service_completed'
AND e.aggregate_type = 'appointment'
AND e.occurred_at >= @pilot_from
AND e.occurred_at < @pilot_to
AND FIND_IN_SET(CAST(e.store_id AS CHAR), @pilot_store_ids) > 0
GROUP BY e.store_id WITH ROLLUP;
-- 5. 指标可靠性反例:四项均须为 0完整发布还必须通过 31 项 production preflight。
SELECT COUNT(*) AS completed_services_without_submitted_report_event_count
FROM t_business_event completed
LEFT JOIN t_report r
ON r.appointment_id = completed.aggregate_id
AND r.store_id = completed.store_id
AND r.deleted = 0
LEFT JOIN t_business_event submitted
ON submitted.store_id = completed.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = r.id
WHERE completed.event_type = 'service_completed'
AND completed.aggregate_type = 'appointment'
AND (r.id IS NULL OR submitted.id IS NULL)
AND completed.occurred_at >= @pilot_from
AND completed.occurred_at < @pilot_to
AND FIND_IN_SET(CAST(completed.store_id AS CHAR), @pilot_store_ids) > 0;
SELECT COUNT(*) AS invalid_rebook_attribution_count
FROM t_follow_up_task t
LEFT JOIN t_appointment a
ON a.id = t.rebooked_appointment_id
AND a.store_id = t.store_id
AND a.deleted = 0
LEFT JOIN t_business_event e
ON e.idempotency_key = CONCAT('rebook_created:', t.rebooked_appointment_id)
AND e.event_type = 'rebook_created'
AND e.store_id = t.store_id
AND e.aggregate_type = 'appointment'
AND e.aggregate_id = t.rebooked_appointment_id
WHERE t.outcome = 'rebooked'
AND (a.id IS NULL OR e.id IS NULL)
AND t.closed_at >= @pilot_from
AND t.closed_at < @pilot_to
AND FIND_IN_SET(CAST(t.store_id AS CHAR), @pilot_store_ids) > 0;
SELECT COUNT(*) AS duplicate_open_follow_up_task_count
FROM (
SELECT t.store_id, t.source_lead_id
FROM t_follow_up_task t
WHERE t.status IN ('pending', 'in_progress')
AND FIND_IN_SET(CAST(t.store_id AS CHAR), @pilot_store_ids) > 0
GROUP BY t.store_id, t.source_lead_id
HAVING COUNT(*) > 1
) duplicated;
SELECT SUM(anomaly_count) AS invalid_metric_event_order_count
FROM (
SELECT COUNT(*) AS anomaly_count
FROM t_business_event sent
JOIN t_business_event submitted
ON submitted.store_id = sent.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = sent.aggregate_id
WHERE sent.event_type = 'report_sent'
AND sent.aggregate_type = 'report'
AND sent.occurred_at < submitted.occurred_at
AND sent.occurred_at >= @pilot_from
AND sent.occurred_at < @pilot_to
AND FIND_IN_SET(CAST(sent.store_id AS CHAR), @pilot_store_ids) > 0
UNION ALL
SELECT COUNT(*) AS anomaly_count
FROM t_business_event rebooked
JOIN t_follow_up_task task
ON task.store_id = rebooked.store_id
AND task.rebooked_appointment_id = rebooked.aggregate_id
JOIN t_business_event submitted_lead
ON submitted_lead.store_id = rebooked.store_id
AND submitted_lead.event_type = 'lead_submitted'
AND submitted_lead.aggregate_type = 'report_lead'
AND submitted_lead.aggregate_id = task.source_lead_id
WHERE rebooked.event_type = 'rebook_created'
AND rebooked.aggregate_type = 'appointment'
AND rebooked.occurred_at < submitted_lead.occurred_at
AND rebooked.occurred_at >= @pilot_from
AND rebooked.occurred_at < @pilot_to
AND FIND_IN_SET(CAST(rebooked.store_id AS CHAR), @pilot_store_ids) > 0
) anomalies;