sql
WITH cohorts AS (
SELECT generate_series(
date_trunc('month', current_date) - interval '11 months',
date_trunc('month', current_date),
interval '1 month'
)::date AS cohort_month
),
cohort_users AS (
SELECT c.cohort_month, u.user_id,
date_trunc('month', u.signup_date)::date AS signup_month
FROM cohorts c
JOIN users u
ON date_trunc('month', u.signup_date)::date = c.cohort_month
),
events_with_offset AS (
SELECT cu.cohort_month,
cu.user_id,
-- if event_date is timestamp with tz: (event_ts AT TIME ZONE 'UTC')::date
date_trunc('month', e.event_date)::date AS event_month,
FLOOR(EXTRACT(epoch FROM (date_trunc('month', e.event_date)::timestamp - date_trunc('month', cu.cohort_month)::timestamp)) / (30*24*3600))::int AS months_since_signup
FROM cohort_users cu
LEFT JOIN events e
ON cu.user_id = e.user_id
WHERE e.event_date IS NULL OR e.event_date >= cu.cohort_month
),
aggregated AS (
SELECT
cohort_month,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 0) AS m0_users,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 1) AS m1_users,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 2) AS m2_users,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 3) AS m3_users,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 4) AS m4_users,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 5) AS m5_users,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 6) AS m6_users,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 7) AS m7_users,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 8) AS m8_users,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 9) AS m9_users,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 10) AS m10_users,
COUNT(DISTINCT user_id) FILTER (WHERE months_since_signup = 11) AS m11_users,
COUNT(DISTINCT user_id) AS cohort_size
FROM events_with_offset
GROUP BY cohort_month
)
SELECT
cohort_month,
ROUND(100.0 * COALESCE(m0_users,0) / NULLIF(cohort_size,0),2) AS pct_m0,
ROUND(100.0 * COALESCE(m1_users,0) / NULLIF(cohort_size,0),2) AS pct_m1,
ROUND(100.0 * COALESCE(m2_users,0) / NULLIF(cohort_size,0),2) AS pct_m2,
ROUND(100.0 * COALESCE(m3_users,0) / NULLIF(cohort_size,0),2) AS pct_m3,
ROUND(100.0 * COALESCE(m4_users,0) / NULLIF(cohort_size,0),2) AS pct_m4,
ROUND(100.0 * COALESCE(m5_users,0) / NULLIF(cohort_size,0),2) AS pct_m5,
ROUND(100.0 * COALESCE(m6_users,0) / NULLIF(cohort_size,0),2) AS pct_m6,
ROUND(100.0 * COALESCE(m7_users,0) / NULLIF(cohort_size,0),2) AS pct_m7,
ROUND(100.0 * COALESCE(m8_users,0) / NULLIF(cohort_size,0),2) AS pct_m8,
ROUND(100.0 * COALESCE(m9_users,0) / NULLIF(cohort_size,0),2) AS pct_m9,
ROUND(100.0 * COALESCE(m10_users,0) / NULLIF(cohort_size,0),2) AS pct_m10,
ROUND(100.0 * COALESCE(m11_users,0) / NULLIF(cohort_size,0),2) AS pct_m11,
cohort_size
FROM aggregated
ORDER BY cohort_month;