Approach: build signup cohorts by users.created_at (cohort_date = date_trunc('day', created_at)), classify cohort as before/after rollout date, compute 30-day retention (users with >=1 session whose started_at <= created_at + interval '30 days') and 30-day LTV (sum of purchases within 30 days divided by cohort size). Treat users with no purchases as contributing $0 to LTV (included in denominator). Key assumptions listed after SQL.sql
WITH params AS (
SELECT
DATE '2024-07-01' AS rollout_date -- set rollout date here
),
users_cohort AS (
SELECT
u.user_id,
date_trunc('day', u.created_at)::date AS cohort_date,
CASE WHEN date_trunc('day', u.created_at)::date < p.rollout_date THEN 'before' ELSE 'after' END AS cohort_side
FROM users u CROSS JOIN params p
),
-- retention: did user have any session within 30 days of signup
user_retention AS (
SELECT
uc.cohort_date,
uc.cohort_side,
uc.user_id,
MAX(CASE WHEN s.started_at <= uc.cohort_date + INTERVAL '30 days' THEN 1 ELSE 0 END) AS retained_30d
FROM users_cohort uc
LEFT JOIN sessions s
ON s.user_id = uc.user_id
AND s.started_at >= uc.cohort_date -- optional: session after signup
GROUP BY 1,2,3
),
-- purchases within 30 days
user_purchases AS (
SELECT
uc.cohort_date,
uc.cohort_side,
uc.user_id,
COALESCE(SUM(CASE WHEN p.purchased_at >= uc.cohort_date AND p.purchased_at < uc.cohort_date + INTERVAL '30 days' THEN p.amount ELSE 0 END),0) AS spend_30d
FROM users_cohort uc
LEFT JOIN purchases p ON p.user_id = uc.user_id
GROUP BY 1,2,3
),
cohort_metrics AS (
SELECT
uc.cohort_date,
uc.cohort_side,
COUNT(DISTINCT uc.user_id) AS cohort_size,
SUM(ur.retained_30d) AS retained_users_30d,
SUM(up.spend_30d) AS total_spend_30d
FROM users_cohort uc
LEFT JOIN user_retention ur ON ur.user_id = uc.user_id AND ur.cohort_date = uc.cohort_date
LEFT JOIN user_purchases up ON up.user_id = uc.user_id AND up.cohort_date = uc.cohort_date
GROUP BY 1,2
)
SELECT
cohort_date,
cohort_side,
cohort_size,
retained_users_30d,
ROUND(100.0 * retained_users_30d / NULLIF(cohort_size,0),2) AS retention_30d_pct,
ROUND(total_spend_30d::numeric / NULLIF(cohort_size,0),2) AS ltv_30d_per_user
FROM cohort_metrics
ORDER BY cohort_date;
Key assumptions and notes:- Cohort defined by sign-up date (day precision). Adjust to week/month if needed.- Retention counts users with at least one session within 30 days after cohort_date; you can require session.started_at >= created_at if sessions may precede signup.- LTV is gross revenue within 30 days divided by cohort size (includes users with zero spend). This provides per-user LTV; if you want per-paying-user LTV, divide by count of users with spend_30d>0 instead.- Timezones: timestamps assumed comparable; apply AT TIME ZONE if needed.- Null/zero handling: use COALESCE and NULLIF to avoid division by zero.- Performance: for large data, pre-aggregate sessions/purchases by user and date ranges or use partition pruning / indexes on user_id and timestamp.