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

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

SourceTables usedNotes
Stripe Sigmasubscription_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 PipelineSame tables under schema STRIPEAdd a merchant_id filter if several accounts share the warehouse.
Fivetransubscription_history (customer_id, status, start_date, trial_end, ended_at, _fivetran_active)Keep _fivetran_active rows only (the current version of each subscription).
Airbytesubscriptions (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):

currencymrr_at_startlost_mrrgross_mrr_churn_pctnet_revenue_retention_pct
usd17150.00610.003.56101.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):

monthcustomers_at_startchurned_customerschurn_pct
2026-09-01412133.16
2026-08-01398153.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):

emailsubscription_idcanceled_atended_at
jo@startup.examplesub_1Q…2026-10-09 14:022026-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):

monthcustomers_cancelled
2026-09-0114
2026-08-0117

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.

TRY IT — FREE, NO SIGNUP
Try one:

Common mistakes

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