Required SQL knowledge: WITH clause, MIN, MAX LEAST and COALESCE functions, Window functions, INNER JOINS, CASE statements, BigQuery arrays manipulation, subqueries.
The main differences with the simplified version are:
Adding inactive months for a customers, between his active subscriptions
Identifying MRR change for a customer
Categorising our MRR change between new, renewal, upgrade, downgrade and churn
In order to model our revenue, we will write a query which will execute the following step:
Retrieve all subscriptions and create a list of all the month between our oldest and most recent subscription dates
Calculate our customer revenue by month
Create our customer churn months
In the end, our modeled dataset will look like this:
Retrieve all subscriptions and create a list of all the month between our oldest and most recent subscription dates
This step is the beginning of our SQL for simplified MRR calculate article, which is:
-- all subscriptionswith subscriptions as (select subscription_id, customer_id, monthly_amount, start_date, end_date, from ${TABLES["Subscriptions"]["subscriptions"]}), -- min and max subscriptions datesdate_limits AS (SELECTMIN(start_date) AS min_date, MAX(end_date) as max_date FROM subscriptions), -- array of month between min and max subscriptions datesmonths_array AS (SELECT GENERATE_DATE_ARRAY(CAST(min_date ASDATE), CAST(max_date ASDATE), INTERVAL 1month ) AS arr FROM date_limits), -- list of months between min and max subscriptions datesmonths as (SELECTCAST (monthasTIMESTAMP) asmonthFROM months_array, UNNEST(months_array.arr) ASmonth),...
Calculate our customer revenue by month
Now for each of our customers we will:
Calculate for each customer is first and last active months.
Create one row per month between his first and last month.
Join our subscriptions with our customer months and set the MRR to 0 for months when a customer subscription is inactive.
Identify for each customer the first_active_month, last_active_month, and for each customer month we add a flag is_first_month and is_last_month.
...-- determine when a given customer had its first and last (or most recent) monthcustomers as (select customer_id, min(start_date) as date_month_start, max(end_date) as date_month_end from subscriptions group by1), -- create one record per month between a customer's first and last monthcustomer_months as (select customers.customer_id, months.month from customers inner join months on months.month >= customers.date_month_start and months.month < customers.date_month_end), -- join the account-month spine to MRR base model, pulling through most recent dates-- and plan info for month rows that have no invoices (i.e. churns)joined as (select customer_months.month, customer_months.customer_id, coalesce(subscriptions.monthly_amount, 0) as mrr from customer_months left join subscriptions on customer_months.customer_id = subscriptions.customer_id and customer_months.month >= subscriptions.start_date and customer_months.month < subscriptions.end_date), customer_revenue_by_month as (select*, first_active_month =monthas is_first_month, last_active_month =monthas is_last_month, from (select*, mrr >0as is_active, min(casewhen mrr >0thenmonthend) over (partitionby customer_id) as first_active_month, max(casewhen mrr >0thenmonthend) over (partitionby customer_id) as last_active_month, from joined )),...
Create our customer churn months
We add a new month after our customer last month in order to create one churn month per customer
...customer_churn_month as (selectcast (date_add(cast (monthasdate), interval 1month) astimestamp) asmonth, customer_id, 0as mrr, false as is_active, first_active_month, last_active_month, false as is_first_month, false as is_last_month from customer_revenue_by_month where is_last_month), ...
Calculate MRR change
Now we will:
Merge our customer_revenue_by_month table with our customer_churn_month table
Get our customer prior month MRR and calculate our current month MRR change
...unioned as (select*from customer_revenue_by_month union allselect*from customer_churn_month), -- get prior month MRR and calculate MRR changemrr_with_changes as (select*, mrr - previous_month_mrr as mrr_changefrom (select*, coalesce(lag(is_active) over (partitionby customer_id order bymonth), false ) as previous_month_is_active,coalesce(lag(mrr) over (partitionby customer_id order bymonth),0 ) as previous_month_mrr,from unioned )),...
Classify our MRR change into categories
Now we have all the data we need to categorize our MRR change. We can easily identify :
New MRR
Churned MRR
Upgrade MRR
Downgrade MRR
Reactivation MRR
...-- classify months as new, churn, reactivation, upgrade, downgrade (or null)mrr_with_changes_categories as (select*, casewhen is_first_month then'new'whennot(is_active) and previous_month_is_active then'churn'when is_active andnot(previous_month_is_active) then'reactivation'when mrr_change >0then'upgrade'when mrr_change <0then'downgrade'endas change_category, least(mrr, previous_month_mrr) as renewal_amount from mrr_with_changes) ...
Assembling the full query
Now we can assemble all the blocks into our full query:
-- all subscriptionswith subscriptions as (select subscription_id, customer_id, monthly_amount, start_date, end_date, from ${TABLES["Subscriptions"]["subscriptions"]}), -- min and max subscriptions datesdate_limits AS (SELECTMIN(start_date) AS min_date, MAX(end_date) as max_date FROM subscriptions), -- array of month between min and max subscriptions datesmonths_array AS (SELECT GENERATE_DATE_ARRAY(CAST(min_date ASDATE), CAST(max_date ASDATE), INTERVAL 1month ) AS arr FROM date_limits), -- list of months between min and max subscriptions datesmonths as (SELECTCAST (monthasTIMESTAMP) asmonthFROM months_array, UNNEST(months_array.arr) ASmonth), -- determine when a given customer had its first and last (or most recent) monthcustomers as (select customer_id, min(start_date) as date_month_start, max(end_date) as date_month_end from subscriptions group by1), -- create one record per month between a customer's first and last monthcustomer_months as (select customers.customer_id, months.month from customers inner join months on months.month >= customers.date_month_start and months.month < customers.date_month_end), -- join the account-month spine to MRR base model, pulling through most recent dates-- and plan info for month rows that have no invoices (i.e. churns)joined as (select customer_months.month, customer_months.customer_id, coalesce(subscriptions.monthly_amount, 0) as mrr from customer_months left join subscriptions on customer_months.customer_id = subscriptions.customer_id and customer_months.month >= subscriptions.start_date and customer_months.month < subscriptions.end_date), customer_revenue_by_month as (select*, first_active_month =monthas is_first_month, last_active_month =monthas is_last_month, from (select*, mrr >0as is_active, min(casewhen mrr >0thenmonthend) over (partitionby customer_id) as first_active_month, max(casewhen mrr >0thenmonthend) over (partitionby customer_id) as last_active_month, from joined )), -- row for month *after* last month of activitycustomer_churn_month as (selectcast (date_add(cast (monthasdate), interval 1month) astimestamp) asmonth, customer_id, 0as mrr, false as is_active, first_active_month, last_active_month, false as is_first_month, false as is_last_month from customer_revenue_by_month where is_last_month), unioned as (select*from customer_revenue_by_month union allselect*from customer_churn_month), -- get prior month MRR and calculate MRR changemrr_with_changes as (select*, mrr - previous_month_mrr as mrr_changefrom (select*, coalesce(lag(is_active) over (partitionby customer_id order bymonth), false ) as previous_month_is_active,coalesce(lag(mrr) over (partitionby customer_id order bymonth),0 ) as previous_month_mrr,from unioned )), -- classify months as new, churn, reactivation, upgrade, downgrade (or null)mrr_with_changes_categories as (select*, casewhen is_first_month then'new'whennot(is_active) and previous_month_is_active then'churn'when is_active andnot(previous_month_is_active) then'reactivation'when mrr_change >0then'upgrade'when mrr_change <0then'downgrade'endas change_category, least(mrr, previous_month_mrr) as renewal_amount from mrr_with_changes) selectmonth, customer_id, mrr, mrr_change, change_category from mrr_with_changes_categories
Run the query and save it (you can use a combination of month and customer_id as primary key).
Build the exploration and the dashboard
By creating an exploration on top on this dataset, we will be able to build the following dashboard: