Approach: compute baseline (training) distributions once (or periodically) and compare daily serving distributions. Store baseline stats in a table baseline_stats(feature, stat, value, bucket) and compute daily_stats from events by day. Use SQL to compute mean/variance, histogram buckets for numeric, category frequencies, and approximate KL divergence. Then produce a daily summary with drift metrics and alert flags.1) Baseline mean/variance (precompute):sql
-- baseline computed over training timeframe
CREATE TABLE baseline_stats AS
SELECT
'feature1' AS feature,
AVG(feature1) AS mean,
VAR_SAMP(feature1) AS var,
MIN(feature1) AS min_v,
MAX(feature1) AS max_v
FROM events
WHERE event_time BETWEEN '2024-01-01' AND '2024-06-30';
2) Daily mean/variance (serving):sql
SELECT
DATE(event_time) AS day,
AVG(feature1) AS mean,
VAR_SAMP(feature1) AS var,
COUNT(*) AS n
FROM events
WHERE event_type='serve'
GROUP BY DATE(event_time);
3) Numeric histogram buckets (baseline + daily) using fixed buckets:sql
-- example bucketized counts
SELECT
period, -- 'baseline' or day
width_bucket(feature1, 0, 100, 20) AS bucket,
COUNT(*) AS cnt
FROM (
SELECT feature1, 'baseline' AS period FROM events WHERE event_time BETWEEN '2024-01-01' AND '2024-06-30'
UNION ALL
SELECT feature1, CAST(DATE(event_time) AS TEXT) FROM events WHERE event_type='serve' AND event_time >= '2024-07-01'
) t
GROUP BY period, bucket;
Use these histograms to compute L1 distance or Chi-square between baseline and daily distributions.4) Mean/variance shift z-score:sql
-- join daily stats to baseline
SELECT d.day,
(d.mean - b.mean) / SQRT(b.var / b_n + d.var / d.n) AS mean_zscore
FROM daily_stats d
JOIN baseline_stats b ON b.feature='feature1'
CROSS JOIN (SELECT COUNT(*) AS b_n FROM events WHERE event_time BETWEEN '2024-01-01' AND '2024-06-30') bn;
Flag |mean_zscore| > 3 as significant shift.5) Categorical frequency and KL-divergence approx:sql
-- baseline category probs
WITH base AS (
SELECT feature2 AS cat, COUNT(*)::float / SUM(COUNT(*)) OVER() AS p
FROM events
WHERE event_time BETWEEN '2024-01-01' AND '2024-06-30'
GROUP BY cat
),
daily AS (
SELECT DATE(event_time) AS day, feature2 AS cat, COUNT(*)::float / SUM(COUNT(*)) OVER(PARTITION BY DATE(event_time)) AS q
FROM events
WHERE event_type='serve' AND event_time >= '2024-07-01'
GROUP BY DATE(event_time), cat
)
SELECT d.day, SUM( COALESCE(d.q,1e-9) * LN( COALESCE(d.q,1e-9) / COALESCE(b.p,1e-9) ) ) AS kl_divergence
FROM (
SELECT day, cat, q FROM daily
) d
LEFT JOIN base b ON d.cat = b.cat
GROUP BY d.day;
Use small epsilon (1e-9) to avoid log(0). KL > threshold (e.g., 0.1) triggers investigation.6) Daily summary table for alerts:sql
CREATE TABLE daily_drift_summary AS
SELECT
d.day,
d.mean AS feature1_mean,
b.mean AS baseline_mean,
ABS(d.mean - b.mean) / NULLIF(b.mean,0) AS mean_rel_change,
mean_zscore,
kl_divergence,
CASE WHEN ABS(d.mean - b.mean) / NULLIF(b.mean,0) > 0.1 OR ABS(mean_zscore) > 3 OR kl_divergence > 0.1 THEN TRUE ELSE FALSE END AS alert
FROM daily_stats d
JOIN baseline_stats b ON b.feature='feature1'
JOIN kl_table k ON k.day = d.day;
Notes/Best practices:- Use same sampling windows and timezone alignment.- For numeric histograms prefer quantile buckets or equal-frequency buckets to avoid empty bins.- Smooth categorical probs (Laplace smoothing) for unseen categories.- Complement SQL with downstream Python job to compute Earth Mover’s Distance or perform permutation tests for significance and to visualize drift.- Store daily_drift_summary in monitoring system and trigger alerts via downstream workflow when alert = TRUE.