Approach: determine for each user and calendar month whether they were active at the month start (i.e., they had a subscription row whose period covers the first day of that month and status = 'active'). Then check whether the same user is active at the first day of the next month. Monthly churn = (users active at month_start but not active at next_month_start) / (users active at month_start).Example (ANSI/BigQuery-style SQL):sql
WITH months AS (
-- generate month starts between data range
SELECT DATE_TRUNC(MIN(period_start), MONTH) + INTERVAL x MONTH AS month_start
FROM subscriptions, UNNEST(GENERATE_ARRAY(0, 60)) AS x -- adjust range as needed
GROUP BY 1
),
user_month_activity AS (
SELECT
m.month_start,
s.user_id,
1 AS active_at_month_start
FROM months m
JOIN subscriptions s
ON DATE(m.month_start) BETWEEN s.period_start AND s.period_end
AND s.status = 'active'
GROUP BY m.month_start, s.user_id
),
user_month_next AS (
SELECT
a.month_start,
a.user_id,
a.active_at_month_start,
CASE WHEN b.user_id IS NOT NULL THEN 1 ELSE 0 END AS active_next_month_start
FROM user_month_activity a
LEFT JOIN user_month_activity b
ON a.user_id = b.user_id
AND DATE_ADD(a.month_start, INTERVAL 1 MONTH) = b.month_start
)
SELECT
month_start,
COUNT(*) AS active_begin,
SUM(CASE WHEN active_next_month_start = 0 THEN 1 ELSE 0 END) AS churned,
SAFE_DIVIDE(SUM(CASE WHEN active_next_month_start = 0 THEN 1 ELSE 0 END), COUNT(*)) AS churn_rate
FROM user_month_next
GROUP BY month_start
ORDER BY month_start;
Key points / reasoning:- Active at month start is defined by a subscription row covering the first day of that month with status='active'.- Reactivations: this calculation treats churn as “not active at next month start” even if the user was active again mid-month — this aligns to many product definitions that consider churn at the next reporting boundary. If you prefer to treat reactivations within the next month as non-churn, adjust to check any active period overlapping the next month (not just the first day).- Partial-periods: using BETWEEN period_start AND period_end ensures partial subscriptions that include the month start count as active. If your product bills daily or prorates mid-month, consider using any overlap with the month (e.g., period_end >= month_start AND period_start < month_end).- Edge cases: users with multiple overlapping rows should be deduped (GROUP BY), cancelled vs. paused statuses should be normalized, and month generation range should cover the data span.