Recipe Tableau and SQL
How do I calculate monthly customer churn and MRR churn?
Customer churn is the customers who paid this month but not the next, divided by everyone who paid this month; MRR churn is the same with their MRR. At this booking-software company, customer churn fell from 5.1% to 3.4% a month after June, while MRR churn rose from 2.6% to 6.9%: fewer customers left, but they were the big ones.
The data
Pawbook subscriptions, 2025
Booking software for dog groomers, billed monthly on three plans: Solo (29), Salon (79) and Multi-site (249). In June a cheaper rival for grooming chains launched.
customer_id | billing_month | plan | mrr |
|---|---|---|---|
| PB-0001 | 2025-01-01 | Solo | 29 |
| PB-0002 | 2025-01-01 | Solo | 29 |
| PB-0003 | 2025-01-01 | Solo | 29 |
| PB-0004 | 2025-01-01 | Solo | 29 |
Step 1
Count paying customers and MRR
The bars climb all year: more groomers pay every month. The line doesn't. MRR peaks in June, then falls three months running while the customer count keeps rising. Something changed in June.
Tableau
COUNTD([customer_id])
// and SUM([mrr]), by MONTH([billing_month])SQL
SELECT
billing_month,
COUNT(DISTINCT customer_id) AS customers,
SUM(mrr) AS mrr
FROM input
WHERE ('{knob.plan}' = 'All' OR plan = '{knob.plan}')
GROUP BY billing_month
ORDER BY billing_monthRows
| Billing month | Customers | MRR |
|---|---|---|
| Jan 2025 | 130 | $10,170 |
| Feb 2025 | 139 | $10,801 |
| Mar 2025 | 140 | $10,930 |
| Apr 2025 | 146 | $11,154 |
| May 2025 | 146 | $11,254 |
| Jun 2025 | 151 | $11,669 |
| Jul 2025 | 155 | $10,735 |
| Aug 2025 | 158 | $10,142 |
| Sep 2025 | 161 | $9,959 |
| Oct 2025 | 170 | $10,320 |
| Nov 2025 | 176 | $10,444 |
| Dec 2025 | 188 | $11,162 |
Step 2
Mark who didn't pay the next month
A customer churns in the last month they paid. The guard on the latest month matters: nobody in December has a January row yet, so without it every December customer looks gone. December is left out on purpose.
Tableau
// Last paid month
{ FIXED [customer_id] : MAX([billing_month]) }
// Latest month in file
{ FIXED : MAX([billing_month]) }
// Churned
[billing_month] = [Last paid month] AND [billing_month] < [Latest month in file]SQL
WITH s AS (
SELECT *
FROM input
WHERE ('{knob.plan}' = 'All' OR plan = '{knob.plan}')
)
SELECT
a.billing_month,
COUNT(*) FILTER (WHERE b.customer_id IS NULL) AS churned,
COALESCE(SUM(a.mrr) FILTER (WHERE b.customer_id IS NULL), 0) AS mrr_lost
FROM s a LEFT JOIN s b ON b.customer_id = a.customer_id AND b.billing_month = a.billing_month + INTERVAL 1 MONTH
WHERE a.billing_month < (
SELECT MAX(billing_month)
FROM input
)
GROUP BY a.billing_month
ORDER BY a.billing_monthRows
| Billing month | Churned | MRR lost |
|---|---|---|
| Jan 2025 | 3 | $137 |
| Feb 2025 | 9 | $361 |
| Mar 2025 | 5 | $195 |
| Apr 2025 | 13 | $477 |
| May 2025 | 6 | $224 |
| Jun 2025 | 8 | $1,432 |
| Jul 2025 | 8 | $1,332 |
| Aug 2025 | 6 | $544 |
| Sep 2025 | 3 | $407 |
| Oct 2025 | 7 | $623 |
| Nov 2025 | 1 | $29 |
Common mistake
Counting the last month as churn
The file ends in December, so no December customer has a next month to pay in. Leave out the latest month, or every customer in it counts as churned and the chart ends in a 100% spike.
Tableau
COUNTD(IF [billing_month] = [Last paid month] THEN [customer_id] END) / COUNTD([customer_id])SQL
SELECT
a.billing_month,
COUNT(*) FILTER (WHERE b.customer_id IS NULL)
/ COUNT(*) AS customer_churn
FROM input a LEFT JOIN input b ON b.customer_id = a.customer_id AND b.billing_month = a.billing_month + INTERVAL 1 MONTH
GROUP BY a.billing_month
ORDER BY a.billing_month DESC
LIMIT 3Tableau
COUNTD(IF [billing_month] = [Last paid month] AND [billing_month] < [Latest month in file] THEN [customer_id] END) / COUNTD([customer_id])SQL
SELECT
a.billing_month,
COUNT(*) FILTER (WHERE b.customer_id IS NULL)
/ COUNT(*) AS customer_churn
FROM input a LEFT JOIN input b ON b.customer_id = a.customer_id AND b.billing_month = a.billing_month + INTERVAL 1 MONTH
WHERE a.billing_month < (
SELECT MAX(billing_month)
FROM input
)
GROUP BY a.billing_month
ORDER BY a.billing_month DESC
LIMIT 3Step 3
Divide by who was paying
Both rates divide by what you had at the start of the month, never by the month's new signups. Until May, MRR churn sits below customer churn: the leavers are small Solo accounts. In June the lines cross, and MRR churn stays on top through October.
Tableau
// Customer churn
COUNTD(IF [Churned] THEN [customer_id] END) / COUNTD([customer_id])
// MRR churn
ZN(SUM(IF [Churned] THEN [mrr] END)) / SUM([mrr])SQL
WITH s AS (
SELECT *
FROM input
WHERE ('{knob.plan}' = 'All' OR plan = '{knob.plan}')
), f AS (
SELECT a.*, b.customer_id IS NULL AS churned
FROM s a LEFT JOIN s b ON b.customer_id = a.customer_id AND b.billing_month = a.billing_month + INTERVAL 1 MONTH
WHERE a.billing_month < (
SELECT MAX(billing_month)
FROM input
)
)
SELECT
billing_month,
COUNT(*) FILTER (WHERE churned) / COUNT(*) AS customer_churn,
COALESCE(SUM(mrr) FILTER (WHERE churned), 0)
/ SUM(mrr) AS mrr_churn
FROM f
GROUP BY billing_month
ORDER BY billing_monthRows
| Billing month | Customer churn | MRR |
|---|---|---|
| Jan 2025 | 0.02 | 0.01 |
| Feb 2025 | 0.06 | 0.03 |
| Mar 2025 | 0.04 | 0.02 |
| Apr 2025 | 0.09 | 0.04 |
| May 2025 | 0.04 | 0.02 |
| Jun 2025 | 0.05 | 0.12 |
| Jul 2025 | 0.05 | 0.12 |
| Aug 2025 | 0.04 | 0.05 |
| Sep 2025 | 0.02 | 0.04 |
| Oct 2025 | 0.04 | 0.06 |
| Nov 2025 | 0.01 | 0 |
Is churn getting better or worse?
Both, depending on which churn you watch. Before June, 5.1% of customers left each month but they took only 2.6% of MRR: mostly one-person Solo shops on the cheapest plan. From June, customer churn improved to 3.4% while MRR churn jumped to 6.9%. A rival built for grooming chains launched that month, and 13 Multi-site accounts left by November. They were 74.1% of the MRR lost in those six months.
Step 4
Before and after June
Pooling the months smooths out the noise of single months. Customer churn improved; MRR churn got much worse. Switch the plan to Multi-site to see where the money went.
Tableau
IF MONTH([billing_month]) < 6 THEN "Jan-May"
ELSE "Jun-Nov"
END
// with both churn ratesSQL
WITH s AS (
SELECT *
FROM input
WHERE ('{knob.plan}' = 'All' OR plan = '{knob.plan}')
), f AS (
SELECT a.*, b.customer_id IS NULL AS churned
FROM s a LEFT JOIN s b ON b.customer_id = a.customer_id AND b.billing_month = a.billing_month + INTERVAL 1 MONTH
WHERE a.billing_month < (
SELECT MAX(billing_month)
FROM input
)
)
SELECT
CASE
WHEN month(billing_month) < 6 THEN 'Jan-May'
ELSE 'Jun-Nov'
END AS period,
COUNT(*) FILTER (WHERE churned) AS churned,
COUNT(*) FILTER (WHERE churned) / COUNT(*) AS customer_churn,
COALESCE(SUM(mrr) FILTER (WHERE churned), 0)
/ SUM(mrr) AS mrr_churn
FROM f
GROUP BY period
ORDER BY periodRows
| Period | Churned | Customer churn | MRR |
|---|---|---|---|
| Jan-May | 36 | 0.05 | 0.03 |
| Jun-Nov | 33 | 0.03 | 0.07 |