Situation: We need reproducible, in-SQL feature engineering that for each user computes:- last 7-day avg usage- days-since-last-activity- number of distinct event types in last 30 days- a 3-point slope of recent usage trendBelow is a compact multi-CTE pipeline using window functions and clear partitioning. Assumptions: source table events(user_id, event_time TIMESTAMP, event_type, usage FLOAT). model_date is the evaluation date (can be max(event_time) or a fixed batch date).sql
WITH
-- 1. determine reference date per user (end of observation window)
user_ref AS (
SELECT
user_id,
MAX(event_time) AS ref_ts,
DATE(MAX(event_time)) AS ref_date
FROM events
GROUP BY user_id
),
-- 2. daily aggregates for each user for the 30-day window before ref_date
daily AS (
SELECT
e.user_id,
DATE(e.event_time) AS day,
SUM(e.usage) AS day_usage,
COUNT(DISTINCT e.event_type) OVER (PARTITION BY e.user_id, DATE(e.event_time)) AS distinct_types_day
FROM events e
JOIN user_ref r
ON e.user_id = r.user_id
AND DATE(e.event_time) BETWEEN DATE_SUB(r.ref_date, INTERVAL 29 DAY) AND r.ref_date
GROUP BY e.user_id, DATE(e.event_time)
),
-- 3. ensure every day in window exists (fill missing days with zero usage)
calendar AS (
SELECT user_id, ref_date, seq.day
FROM user_ref
CROSS JOIN (
SELECT 0 AS d UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14
UNION ALL SELECT 15 UNION ALL SELECT 16 UNION ALL SELECT 17 UNION ALL SELECT 18 UNION ALL SELECT 19
UNION ALL SELECT 20 UNION ALL SELECT 21 UNION ALL SELECT 22 UNION ALL SELECT 23 UNION ALL SELECT 24
UNION ALL SELECT 25 UNION ALL SELECT 26 UNION ALL SELECT 27 UNION ALL SELECT 28 UNION ALL SELECT 29
) AS seq(d)
CROSS APPLY (SELECT DATE_SUB(ref_date, INTERVAL seq.d DAY) AS day)
),
daily_full AS (
SELECT
c.user_id,
c.day,
COALESCE(d.day_usage, 0) AS day_usage,
COALESCE(d.distinct_types_day, 0) AS distinct_types_day
FROM calendar c
LEFT JOIN daily d
ON c.user_id = d.user_id AND c.day = d.day
),
-- 4. compute rolling features using window frames partitioned by user ordered by day
features_per_day AS (
SELECT
user_id,
day,
day_usage,
-- last 7-day average usage (inclusive of current day)
AVG(day_usage) OVER (
PARTITION BY user_id
ORDER BY day
RANGE BETWEEN INTERVAL 6 DAY PRECEDING AND CURRENT ROW
) AS avg_7d_usage,
-- count distinct event types over last 30 days: sum of per-day distinct_types_day over window
SUM(distinct_types_day) OVER (
PARTITION BY user_id
ORDER BY day
RANGE BETWEEN INTERVAL 29 DAY PRECEDING AND CURRENT ROW
) AS distinct_event_types_30d,
-- for 3-point slope: take day_usage at day (t), t-1, t-2 and compute slope = (y_t - y_{t-2}) / 2
LAG(day_usage, 1) OVER (PARTITION BY user_id ORDER BY day) AS y_t1,
LAG(day_usage, 2) OVER (PARTITION BY user_id ORDER BY day) AS y_t2
FROM daily_full
),
-- 5. pick only rows corresponding to each user's ref_date (end of window) to produce final features
final_features AS (
SELECT
u.user_id,
f.day AS ref_date,
f.avg_7d_usage,
-- days since last activity: difference between ref_ts and last actual event_time
DATE_DIFF(u.ref_date, MAX(e.event_time), DAY) OVER (PARTITION BY u.user_id) AS days_since_last_activity,
f.distinct_event_types_30d,
-- 3-point slope (average daily change over last 2-day span). If y_t2 is NULL, set NULL.
CASE
WHEN f.y_t2 IS NULL THEN NULL
ELSE (f.day_usage - f.y_t2) / 2.0
END AS slope_3point
FROM user_ref u
JOIN features_per_day f
ON u.user_id = f.user_id AND u.ref_date = f.day
LEFT JOIN events e
ON e.user_id = u.user_id AND DATE(e.event_time) = u.ref_date
GROUP BY u.user_id, f.day, f.avg_7d_usage, f.distinct_event_types_30d, f.day_usage, f.y_t2
)
SELECT * FROM final_features;
Key reasoning and window/frame choices:- Partition BY user_id isolates per-user time series (no leakage across users).- ORDER BY day with RANGE BETWEEN INTERVAL n DAY PRECEDING is used because windows are time-based (handles missing days transparently when we expanded calendar).- AVG over 7-day RANGE ensures we average the last 7 calendar days ending at ref_date.- For distinct event-types in 30 days we sum per-day distinct counts over a 30-day RANGE (acceptable if event types don't repeat within day; else use APPROX_COUNT_DISTINCT over raw events partitioned).- 3-point slope uses lag 1 and 2 to get last 3 consecutive days; slope = (y_t - y_{t-2})/2 approximates linear trend. If you need robust slope, run an OLS over last k points using windowed SUMs (Σx, Σy, Σxy, Σx2).- days-since-last-activity computed against last event_time per user (use ref_ts vs last activity timestamp).Edge considerations: timezone normalization, sparse activity (NULL handling), large cardinality — pre-aggregate events and use partitioning/clustered storage for performance.