Approach (brief): for each date compute the number of distinct users seen in that date and the previous 6 days. Exact SQL is straightforward but can be expensive; at scale prefer pre-aggregation and/or approximate distinct (HLL).Exact (simple, correct but can be slow on large tables) — one row per date using a correlated subquery:sql
SELECT d.day,
(SELECT COUNT(DISTINCT user_id)
FROM daily_activity a
WHERE a.date BETWEEN d.day - INTERVAL '6 days' AND d.day
) AS rolling_7d_active_users
FROM (
SELECT DISTINCT date::date AS day
FROM daily_activity
) d
ORDER BY d.day;
More efficient pre-aggregation + union of daily unique sets (example using PostgreSQL + intarray/bitmap extension idea):1) Pre-aggregate daily uniques into a deduplicated table:sql
CREATE MATERIALIZED VIEW daily_uniques AS
SELECT date::date AS day, user_id
FROM daily_activity
GROUP BY 1, user_id;
2) Then compute rolling by joining 7 days of pre-aggregated rows:sql
SELECT d.day,
COUNT(DISTINCT du.user_id) AS rolling_7d_active_users
FROM (SELECT DISTINCT day FROM daily_uniques) d
JOIN daily_uniques du
ON du.day BETWEEN d.day - INTERVAL '6 days' AND d.day
GROUP BY d.day
ORDER BY d.day;
Approximate / large scale (recommended for dashboards with many dates): use HyperLogLog or native approximate functions (BigQuery, Redshift, ClickHouse).BigQuery example (fast, memory-friendly):sql
SELECT day,
APPROX_COUNT_DISTINCT(user_id) /* single-day approx */
OVER (PARTITION BY 1 ORDER BY day
RANGE BETWEEN INTERVAL 6 DAY PRECEDING AND CURRENT ROW) AS rolling_7d_hll
FROM (
SELECT DATE(date) AS day, user_id FROM daily_activity
)
GROUP BY day, user_id
ORDER BY day;
Alternatively build an HLL sketch per day and merge sketches for each 7-day window:sql
-- pseudo BigQuery-style with HLL functions
WITH daily_hll AS (
SELECT day, HLL_AGG(user_id) AS hll_sketch
FROM (SELECT DATE(date) day, user_id FROM daily_activity)
GROUP BY day
)
SELECT d.day,
HLL_COUNT(HLL_UNION_AGG(hll_sketch)) AS rolling_7d_approx
FROM daily_hll d
JOIN daily_hll h ON h.day BETWEEN d.day - 6 AND d.day
GROUP BY d.day
ORDER BY d.day;
Trade-offs and performance guidance:- Exact COUNT(DISTINCT) over sliding windows is correct but expensive; it requires scanning or joining many rows and heavy distinct aggregation.- Pre-aggregate daily uniques (materialized views, partitioned tables) to reduce input size by deduplicating per-day.- Use sketches (HLL) / APPROX_COUNT_DISTINCT for speed and low memory; error ~1-2% acceptable for dashboards.- Use partitioning on date, clustering on user_id, and proper indexes to accelerate joins.- Maintain incremental pipelines: update daily sketches or daily_uniques each day and compute rolling metrics incrementally rather than scanning full history.- If business requires exact counts for short windows, consider bitmaps (Roaring/bitmap indexes) to union efficiently per window.Edge cases:- Days with no activity should appear as zero (ensure calendar table).- Timezones: normalize timestamps to a date.- Late-arriving events: decide retention and backfill policy; recompute affected windows when updating historical partitions.