Stripe Churn Rate SQL: Customer and Revenue Churn
For founders who want a churn number they can defend, computed from their own Stripe data. You get customer churn, gross MRR churn and net revenue retention for last month, a 12-month trend, and lists of who cancelled and who is about to.
· AI2SQL
- Stripe Sigma
- Stripe Data Pipeline
- Fivetran
- Airbyte
Quick answer
Customer (logo) churn for last month. A customer counts as active when their MRR is above zero, which is Stripe’s own definition, so trials and unpaid sign-ups stay out.
-- Stripe Sigma (Trino)
-- Customer churn rate for last calendar month
with per_customer as (
select
customer_id,
sum(case when local_event_timestamp < date_trunc('month', current_date) - interval '1' month then mrr_change else 0 end) as mrr_start,
sum(case when local_event_timestamp < date_trunc('month', current_date) then mrr_change else 0 end) as mrr_end
from subscription_item_change_events_v2_beta
group by customer_id
)
select
count_if(mrr_start > 0) as customers_at_start,
count_if(mrr_start > 0 and mrr_end = 0) as churned_customers,
round(100.0 * count_if(mrr_start > 0 and mrr_end = 0) / nullif(count_if(mrr_start > 0), 0), 2) as customer_churn_pct
from per_customer
Schema assumptions
| Source | Tables used | Notes |
|---|---|---|
| Stripe Sigma | subscription_item_change_events_v2_beta (customer_id, currency, mrr_change, local_event_timestamp), subscriptions (id, customer_id, status, canceled_at, ended_at, cancel_at_period_end, cancel_at), customers (id, email) | mrr_change in minor units. |
| Stripe Data Pipeline | Same tables under schema STRIPE | Add a merchant_id filter if several accounts share the warehouse. |
| Fivetran | subscription_history (customer_id, status, start_date, trial_end, ended_at, _fivetran_active) | Keep _fivetran_active rows only (the current version of each subscription). |
| Airbyte | subscriptions (customer, status, created, canceled_at, ended_at, trial_end) | Timestamps are Unix epoch seconds; wrap them in to_timestamp() on Postgres. |
Copy-ready SQL
1. Gross MRR churn and net revenue retention
How much recurring revenue last month’s existing customers took with them, and what they are worth now. Change: nothing. Rows are per currency.
-- Stripe Sigma (Trino)
with per_customer as (
select
customer_id,
currency,
sum(case when local_event_timestamp < date_trunc('month', current_date) - interval '1' month then mrr_change else 0 end) as mrr_start,
sum(case when local_event_timestamp < date_trunc('month', current_date) then mrr_change else 0 end) as mrr_end
from subscription_item_change_events_v2_beta
group by customer_id, currency
)
select
currency,
sum(mrr_start) / 100.0 as mrr_at_start,
sum(greatest(mrr_start - mrr_end, 0)) / 100.0 as lost_mrr,
round(100.0 * sum(greatest(mrr_start - mrr_end, 0)) / nullif(sum(mrr_start), 0), 2) as gross_mrr_churn_pct,
round(100.0 * sum(mrr_end) / nullif(sum(mrr_start), 0), 2) as net_revenue_retention_pct
from per_customer
where mrr_start > 0
group by currency
Example output (made-up data):
| currency | mrr_at_start | lost_mrr | gross_mrr_churn_pct | net_revenue_retention_pct |
|---|---|---|---|---|
| usd | 17150.00 | 610.00 | 3.56 | 101.20 |
2. Customer churn rate for the last 12 months
The trend, one row per month. Change: interval '12' month for a longer window.
-- Stripe Sigma (Trino)
with months as (
select month
from unnest(sequence(
date_trunc('month', current_date) - interval '12' month,
date_trunc('month', current_date) - interval '1' month,
interval '1' month
)) as t (month)
),
per_customer_month as (
select
m.month,
e.customer_id,
sum(case when e.local_event_timestamp < m.month then e.mrr_change else 0 end) as mrr_start,
sum(case when e.local_event_timestamp < m.month + interval '1' month then e.mrr_change else 0 end) as mrr_end
from months m
cross join subscription_item_change_events_v2_beta e
group by 1, 2
)
select
month,
count_if(mrr_start > 0) as customers_at_start,
count_if(mrr_start > 0 and mrr_end = 0) as churned_customers,
round(100.0 * count_if(mrr_start > 0 and mrr_end = 0) / nullif(count_if(mrr_start > 0), 0), 2) as churn_pct
from per_customer_month
group by month
order by month desc
Example output (made-up data):
| month | customers_at_start | churned_customers | churn_pct |
|---|---|---|---|
| 2026-09-01 | 412 | 13 | 3.16 |
| 2026-08-01 | 398 | 15 | 3.77 |
3. Customers who cancelled in the last 30 days
Who left, so you can email them. Change: the 30-day window.
-- Stripe Sigma (Trino)
select
c.email,
s.id as subscription_id,
s.canceled_at,
s.ended_at
from subscriptions s
join customers c on c.id = s.customer_id
where s.canceled_at >= current_date - interval '30' day
order by s.canceled_at desc
Example output (made-up data):
| subscription_id | canceled_at | ended_at | |
|---|---|---|---|
| jo@startup.example | sub_1Q… | 2026-10-09 14:02 | 2026-10-31 00:00 |
4. Customers scheduled to cancel
Already cancelled, still paying until the period ends. This is your save list. Change: nothing.
-- Stripe Sigma (Trino)
select
c.email,
s.id as subscription_id,
s.cancel_at as ends_on
from subscriptions s
join customers c on c.id = s.customer_id
where s.status = 'active'
and s.cancel_at_period_end
order by s.cancel_at
5. Customer churn rate from Fivetran tables (Postgres)
Same definition, built from subscription dates because Fivetran has no MRR change log. A customer is active on a date if any of their subscriptions had started, finished its trial and not ended. Change: the stripe schema name.
-- PostgreSQL (Fivetran Stripe schema "stripe")
with bounds as (
select
date_trunc('month', current_date) - interval '1 month' as month_start,
date_trunc('month', current_date) as month_end
),
per_customer as (
select
s.customer_id,
bool_or(coalesce(s.trial_end, s.start_date) < b.month_start
and (s.ended_at is null or s.ended_at >= b.month_start)) as active_at_start,
bool_or(coalesce(s.trial_end, s.start_date) < b.month_end
and (s.ended_at is null or s.ended_at >= b.month_end)) as active_at_end
from stripe.subscription_history s
cross join bounds b
where s._fivetran_active
group by s.customer_id
)
select
count(*) filter (where active_at_start) as customers_at_start,
count(*) filter (where active_at_start and not active_at_end) as churned_customers,
round(100.0 * count(*) filter (where active_at_start and not active_at_end)
/ nullif(count(*) filter (where active_at_start), 0), 2) as customer_churn_pct
from per_customer;
6. Cancellations per month from Airbyte tables (Postgres)
Airbyte keeps Stripe’s Unix timestamps, so convert them first. Change: the airbyte_stripe schema name.
-- PostgreSQL (Airbyte Stripe schema "airbyte_stripe")
select
date_trunc('month', to_timestamp(canceled_at)) as month,
count(distinct customer) as customers_cancelled
from airbyte_stripe.subscriptions
where canceled_at is not null
and livemode
group by 1
order by 1 desc;
Example output (made-up data):
| month | customers_cancelled |
|---|---|
| 2026-09-01 | 14 |
| 2026-08-01 | 17 |
Run it on your own database with AI2SQL
If Fivetran or Airbyte syncs your Stripe data into Postgres, MySQL or SQL Server, connect that database read-only and ask in plain English. For Sigma, Snowflake or BigQuery, AI2SQL writes the query in that dialect and you run it there.
Free to try. See pricing for what each plan includes.
Want to run this on your database — with your real tables?
Save & run on your database →Common mistakes
- Trials counted as churn. A trial that never converts is a failed conversion, not a churned customer. Start the denominator from paying customers. Stripe's change-events table does this for you because trials have no MRR.
- Cents, not dollars. mrr_change and every Stripe amount are in minor units. Divide by 100 for USD or EUR, and keep currencies apart until you convert them.
- Dividing by the wrong base. Monthly churn is churned customers divided by customers active at the start of the month, not at the end and not including the month's new signups.
- Counting subscriptions instead of customers. A customer who swaps plans ends one subscription and starts another. Count customers whose MRR went to zero, not ended subscriptions, or plan changes look like churn.
- Month boundaries in the wrong timezone. Use local_event_timestamp for months in your account's timezone, or event_timestamp for UTC. Fivetran stores Stripe timestamps in the account timezone; Airbyte stores Unix epoch seconds.
- Test mode mixed in. Check that you are querying live data: Data Pipeline puts test data in STRIPE_TESTMODE, and Fivetran's Stripe Test Mode is a separate connector.
Frequently asked questions
How do I calculate churn rate from Stripe in SQL? + −
Customer churn rate = customers paying at the start of the month who are no longer paying at the end, divided by customers paying at the start. In Sigma, sum mrr_change per customer before each boundary from subscription_item_change_events_v2_beta: MRR above zero at the start and zero at the end is a churned customer.
What is the difference between customer churn and revenue churn? + −
Customer (logo) churn counts accounts lost. Gross MRR churn is MRR lost to cancellations and downgrades divided by MRR at the start of the month. Net revenue retention adds expansion back in, so it can go above 100% when existing customers grow.
Does Stripe's dashboard churn match this SQL? + −
Close, if you use the same definitions. Stripe counts an active subscriber as a customer with non-zero MRR, which is what these queries do. Differences usually come from currency conversion, timezone, or the last two days of data still settling.
How do I find customers who are about to churn? + −
Query subscriptions where status is active and cancel_at_period_end is true. They have already cancelled and will end at cancel_at (Stripe sets it to the period end). Past-due subscriptions are the other early warning; see the failed payments template.
Can I run these queries on Fivetran or Airbyte data? + −
Yes, with different tables. Fivetran has subscription_history with start_date, ended_at and trial_end; Airbyte has subscriptions with the same fields as Unix epoch integers. Both versions are below.
Related templates
All templates: SQL templates for Stripe, Shopify and Supabase