Approach:Filter users who had a 'signup' event in the last 30 days, then for each user find whether they completed the next funnel steps and count unique users at each step. Compute step-to-step and overall conversion percentages.sql
WITH signup_users AS (
-- users who signed up in the last 30 days, take earliest signup time per user
SELECT user_id, MIN(event_time) AS signup_time
FROM events
WHERE event_name = 'signup'
AND event_time >= now() - interval '30 days'
GROUP BY user_id
),
user_steps AS (
-- for each user, find the earliest time they completed each step after signup
SELECT
s.user_id,
s.signup_time,
MIN(CASE WHEN e.event_name = 'onboard_complete' AND e.event_time >= s.signup_time THEN e.event_time END) AS onboard_time,
MIN(CASE WHEN e.event_name = 'first_purchase' AND e.event_time >= s.signup_time THEN e.event_time END) AS purchase_time
FROM signup_users s
LEFT JOIN events e
ON e.user_id = s.user_id
GROUP BY s.user_id, s.signup_time
),
aggregates AS (
SELECT
COUNT(*) FILTER (WHERE signup_time IS NOT NULL) AS signup_count,
COUNT(*) FILTER (WHERE onboard_time IS NOT NULL) AS onboard_count,
COUNT(*) FILTER (WHERE purchase_time IS NOT NULL) AS purchase_count
FROM user_steps
)
SELECT
signup_count,
onboard_count,
purchase_count,
ROUND(100.0 * onboard_count / NULLIF(signup_count,0), 2) AS pct_signup_to_onboard,
ROUND(100.0 * purchase_count / NULLIF(onboard_count,0), 2) AS pct_onboard_to_purchase,
ROUND(100.0 * purchase_count / NULLIF(signup_count,0), 2) AS pct_signup_to_purchase
FROM aggregates;
Key points:- We anchor events relative to each user's signup_time to ensure correct order.- Use MIN(...) to capture first completion of each step.- NULLIF avoids division-by-zero; percentages rounded.Edge cases:- Users with multiple signups: we use earliest signup in last 30 days.- Events before signup are ignored.- If you want strict ordering (onboard must occur before purchase), confirm purchase_time >= onboard_time in user_steps filter.Alternative:- Use window functions or EXISTS-based counts if dataset is large; consider indexing on (event_name, event_time, user_id) for performance.