Approach: define occurred_date = DATE(occurred_at), compute delay = arrived_at - occurred_at in hours. For each occurred_date, compute fraction of events that arrived >24h. Then compute 90-day trend including daily fraction and a 7-day rolling average; optionally compute distribution of delays (e.g., >24h, 24–48h, 48–72h, >72h).Per-day late fraction:sql
WITH events_by_occurred AS (
SELECT
DATE(occurred_at) AS occurred_date,
COUNT(*) AS total_events,
SUM(CASE WHEN TIMESTAMP_DIFF(arrived_at, occurred_at, HOUR) > 24 THEN 1 ELSE 0 END) AS late_events
FROM events
GROUP BY 1
)
SELECT
occurred_date,
total_events,
late_events,
SAFE_DIVIDE(late_events, total_events) AS late_fraction
FROM events_by_occurred
ORDER BY occurred_date;
90-day trend with daily fraction and 7-day rolling avg:sql
WITH daily AS (
SELECT
DATE(occurred_at) AS occurred_date,
COUNT(*) AS total_events,
SUM(CASE WHEN TIMESTAMP_DIFF(arrived_at, occurred_at, HOUR) > 24 THEN 1 ELSE 0 END) AS late_events
FROM events
WHERE DATE(occurred_at) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 89 DAY) AND CURRENT_DATE()
GROUP BY 1
)
SELECT
occurred_date,
total_events,
late_events,
SAFE_DIVIDE(late_events, total_events) AS late_fraction,
-- 7-day rolling average of late_fraction
AVG(SAFE_DIVIDE(late_events, NULLIF(total_events,0))) OVER (
ORDER BY occurred_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS late_fraction_7d_avg
FROM daily
ORDER BY occurred_date;
Delay-bucket distribution (helpful to monitor changes):sql
SELECT
DATE(occurred_at) AS occurred_date,
SUM(CASE WHEN diff_h <= 24 THEN 1 ELSE 0 END) AS ontime,
SUM(CASE WHEN diff_h BETWEEN 25 AND 48 THEN 1 ELSE 0 END) AS late_24_48,
SUM(CASE WHEN diff_h BETWEEN 49 AND 72 THEN 1 ELSE 0 END) AS late_48_72,
SUM(CASE WHEN diff_h > 72 THEN 1 ELSE 0 END) AS late_over_72,
COUNT(*) AS total,
SAFE_DIVIDE(SUM(CASE WHEN diff_h > 24 THEN 1 ELSE 0 END), COUNT(*)) AS late_fraction
FROM (
SELECT *, TIMESTAMP_DIFF(arrived_at, occurred_at, HOUR) AS diff_h
FROM events
WHERE DATE(occurred_at) >= DATE_SUB(CURRENT_DATE(), INTERVAL 89 DAY)
) t
GROUP BY 1
ORDER BY occurred_date;
Why this works:- Uses occurred_date as the reporting date and arrived_at to classify lateness.- SAFE_DIVIDE/NULLIF avoids divide-by-zero.- Rolling average smooths noise and reveals trend shifts.Edge cases:- Null timestamps, negative delays (future-arrival) — filter or flag them.- Timezone consistency: ensure arrived_at/occurred_at in same timezone or normalized to UTC.Alternatives:- Use percentiles of delay per day to detect tail shifts.- Compute cumulative late arrivals by ingestion date to see backfill velocity.