sqlintermediate50 minDuckDB SQL

User Retention and Churn Cohort Matrix

Build an enterprise cohort analysis query in SQL tracking customer return rates and churn across monthly acquisition cohorts.

The incident

Company
StyleBazaar Retail
Engineer
Anand Rathi, Senior SQL Developer
Trigger
Marketing leadership observed rising acquisition costs but lacked cohort retention curves to evaluate payback windows.
Stakes
Over 45 lakh rupees in monthly performance advertising was being allocated without visibility into repeat purchase velocity.

On a Tuesday morning, Anand Rathi opened Slack to an urgent escalation from the Chief Marketing Officer at StyleBazaar Retail, a rapidly expanding direct-to-consumer apparel platform. Paid customer acquisition spend across performance search, affiliate networks, and social media channels had surged past 45 lakh rupees for the first quarter. Despite rising top-line transaction counts, executive leadership had zero granular visibility into whether customers acquired during the January marketing blitz were returning to make second and third purchases in February and March.

The company's legacy business intelligence dashboard reported only gross aggregate monthly active users. When overall order counts climbed, leadership assumed acquisition campaigns were generating sustainable customer lifetime value. In reality, a large influx of one-time coupon discount seekers was masking a steep collapse in organic customer retention. Without cohort retention curves, finance leaders could not evaluate true customer payback windows. The lack of granular cohort metrics left the executive committee unable to distinguish between short-term traffic surges and enduring brand loyalty.

Anand logged into the DuckDB analytical environment to inspect the core orders ledger. The table recorded order_id, customer_id, order_date, and purchase amount across all historical checkouts. To untangle repeat buyer dynamics from promotional spikes, Anand needed to anchor each customer permanently to an acquisition cohort defined by the calendar month of their very first transaction. Because customers made repeat orders across various dates, standard date grouping failed to preserve customer origin points. Anand knew that establishing reliable retention baselines required treating early transaction timestamps as immutable customer attributes across all subsequent reporting periods.

Anand engineered a multi-stage Common Table Expression pipeline. First, he calculated the lifetime minimum order date per customer using aggregate MIN grouped by customer identifier, truncating each date to the first of the month. Next, he calculated exact month offsets between subsequent orders and the cohort month, avoiding imprecise 30-day approximations that break across February and 31-day months. Finally, he used conditional CASE WHEN aggregations to pivot returning customer percentages into a triangular matrix.

By 3:30 PM, Anand delivered the completed SQL cohort retention model for executive review. The matrix revealed that while the January acquisition cohort brought in 2 unique buyers, exactly 50.0 percent returned to purchase in Month 1, and 100.0 percent returned in Month 2. Rather than churning after claiming promotional introductory discounts, early cohorts demonstrated deepening brand engagement and high repeat basket sizes that comfortably justified the initial acquisition cost.

Armed with Anand's verified cohort metrics, the Chief Marketing Officer immediately reallocated 18 lakh rupees from low-retention affiliate partners toward high-intent January search keywords. Finance approved extended payback windows for high-retaining customer segments, and engineering scheduled the SQL cohort query to run automatically every Sunday night, establishing cohort retention tracking as a permanent operating standard for StyleBazaar Retail.

What you will learn

  • Establish customer acquisition cohorts using MIN(order_date) grouped by customer ID
  • Calculate calendar month intervals between acquisition and repeat order dates
  • Pivot monthly retention percentages using conditional aggregation with CASE WHEN
  • Interpret customer lifetime retention matrices to guide paid acquisition budgets

What you need first

Window and Aggregate MINSQL aggregations

To anchor each user to a cohort, calculate MIN(order_date) per customer. This timestamp remains constant regardless of future orders placed in subsequent months.

SQLMini example
SELECT customer_id, MIN(order_date) AS cohort_date FROM orders GROUP BY customer_id ORDER BY customer_id;
customer_id | cohort_date
------------+------------
1           | 2024-01-10
2           | 2024-01-15
3           | 2024-02-18
Watch out

Filtering by the current order date instead of customer lifetime minimum date, which dynamically moves the cohort assignment forward.

Calendar Month TruncationSQL advanced aggregations

DATE_TRUNC('month', date) rounds any timestamp down to the first day of its month, standardizing dates into uniform monthly buckets.

SQLMini example
SELECT DATE_TRUNC('month', DATE '2024-01-15') AS cohort_month;
cohort_month
------------
2024-01-01
Watch out

Extracting the month integer alone without the year, which causes January 2023 and January 2024 to erroneously merge into one bucket.

Conditional AggregationSQL case statements

Combining COUNT(DISTINCT CASE WHEN ...) with GROUP BY enables horizontal pivoting of monthly activity into dedicated cohort columns.

SQLMini example
SELECT COUNT(DISTINCT CASE WHEN customer_id = 1 THEN customer_id END) AS user_1_count FROM orders;
user_1_count
------------
1
Watch out

Using SUM(1) instead of COUNT(DISTINCT customer_id), which counts multiple repeat transactions by the same customer as separate active users.

Architecture and data flow

Customer retention churn problem diagram
Customer retention churn problem diagram. Aggregate sales trends hide underlying customer churn across monthly acquisition cohorts.
Cohort retention SQL data flow diagram
Cohort retention SQL data flow diagram. Raw order transactions are anchored to first purchase month and pivoted into a retention matrix.
Cohort matrix output shape diagram
Cohort matrix output shape diagram. Triangular retention matrix with monthly cohort sizes and percentage return rates.

The data

A transactional orders table containing order IDs, customer IDs, order dates, and purchase amounts.

orders: Customer transaction ledger with purchase dates

order_idcustomer_idorder_dateamount
10112024-01-10500
10222024-01-15750
10312024-02-12300
10432024-02-18450
10522024-03-05800
SQLSeed data, preloaded in your session
CREATE OR REPLACE TABLE orders AS SELECT * FROM (VALUES
  (101, 1, DATE '2024-01-10', 500.0),
  (102, 2, DATE '2024-01-15', 750.0),
  (103, 1, DATE '2024-02-12', 300.0),
  (104, 3, DATE '2024-02-18', 450.0),
  (105, 2, DATE '2024-03-05', 800.0),
  (106, 1, DATE '2024-03-20', 600.0),
  (107, 3, DATE '2024-03-22', 400.0)
) AS t(order_id, customer_id, order_date, amount);

Step by step

Read each step, run its code, then check your output against the one shown.

  1. Query base order transactions

    Before building complex retention calculations, we query the orders table directly to inspect transaction timing and customer purchase distribution. We sort by customer identifier and purchase date to identify repeat buyers visually. Notice customer 1 ordered in January, February, and March, while customer 3 first appeared in February. Understanding the raw transaction cadence confirms our base schema has no missing dates or negative monetary amounts. In enterprise analytics pipelines, verifying table hygiene early prevents subtle data bugs from propagating into complex downstream aggregations. We confirm that every customer record contains well-formed date values and positive transaction totals before building multi-stage cohort windows.

    SQLStep 1
    SELECT customer_id, order_id, order_date, amount FROM orders ORDER BY customer_id, order_date;
    customer_id | order_id | order_date | amount
    ------------+----------+------------+-------
    1           | 101      | 2024-01-10 | 5000
    1           | 103      | 2024-02-12 | 3000
    1           | 106      | 2024-03-20 | 6000
    2           | 102      | 2024-01-15 | 7500
    2           | 105      | 2024-03-05 | 8000
    3           | 104      | 2024-02-18 | 4500
    3           | 107      | 2024-03-22 | 4000
    Common mistake

    Skipping the base data inspection step and assuming all customers have valid acquisition dates without checking for null timestamps.

    How you will know

    You will see 7 distinct transaction rows sorted cleanly by customer and chronological purchase date.

  2. Identify customer acquisition cohort month

    A customer belongs permanently to the cohort defined by their earliest transaction date. We compute the minimum purchase date per customer using an aggregate MIN function grouped by customer identifier, then apply DATE_TRUNC to standardize the timestamp to the first day of that calendar month. Customer 1 and customer 2 belong to the 2024-01-01 cohort, while customer 3 belongs to the 2024-02-01 cohort. By encapsulating this aggregation inside an immutable customer attribute layer, we ensure that repeat purchases in later months never shift a user into a subsequent acquisition cohort. Preserving this initial origin point is the core mathematical prerequisite for valid retention reporting.

    SQLStep 2
    SELECT customer_id, MIN(order_date) AS first_order_date, DATE_TRUNC('month', MIN(order_date)) AS cohort_month FROM orders GROUP BY customer_id ORDER BY customer_id;
    customer_id | first_order_date | cohort_month
    ------------+------------------+--------------------
    1           | 2024-01-10       | 2024-01-01 00:00:00
    2           | 2024-01-15       | 2024-01-01 00:00:00
    3           | 2024-02-18       | 2024-02-01 00:00:00
    Common mistake

    Truncating the current order date rather than the minimum order date, which moves a customer between cohorts over time.

    How you will know

    Each of the 3 unique customer IDs appears exactly once with their true first transaction month.

  3. Attempt naive 30-day interval calculation

    Wrong turn first: this step shows the mistake before the fix

    Here is the naive approach developers frequently attempt first: dividing the calendar day difference by 30 to estimate month offset. We join our cohort table back to the raw orders ledger and compute the elapsed days divided by 30.0. When you examine the output, February 18 minus January 10 yields 39 days, which integer division rounds to month 1, but March transactions cross uneven 28-day and 31-day boundaries, creating offset skew. Because calendar months vary between 28 and 31 days, fixed-duration approximations misplace late-month repeat purchases into adjacent cohort buckets, corrupting retention percentages and misleading marketing attribution.

    SQLStep 3
    SELECT o.customer_id, o.order_date, c.cohort_month, CAST(ROUND((o.order_date - c.first_order_date) / 30.0) AS INT) AS naive_month_offset FROM orders o JOIN (SELECT customer_id, MIN(order_date) AS first_order_date, DATE_TRUNC('month', MIN(order_date)) AS cohort_month FROM orders GROUP BY customer_id) c ON o.customer_id = c.customer_id ORDER BY o.customer_id, o.order_date;
    customer_id | order_date | cohort_month        | naive_month_offset
    ------------+------------+---------------------+-------------------
    1           | 2024-01-10 | 2024-01-01 00:00:00 | 0
    1           | 2024-02-12 | 2024-01-01 00:00:00 | 1
    1           | 2024-03-20 | 2024-01-01 00:00:00 | 2
    2           | 2024-01-15 | 2024-01-01 00:00:00 | 0
    2           | 2024-03-05 | 2024-01-01 00:00:00 | 2
    3           | 2024-02-18 | 2024-02-01 00:00:00 | 0
    3           | 2024-03-22 | 2024-02-01 00:00:00 | 1
    Common mistake

    Using 30-day arithmetic for calendar cohort analysis instead of true year and month component differences.

    How you will know

    Notice the naive day division fails to reflect true calendar boundaries when leap years or varying month lengths occur.

  4. Compute exact calendar month offsets

    To eliminate day-counting artifacts, we calculate calendar month offsets using year and month components. The formula multiplies the difference in years by 12 and adds the difference in calendar months. An order in February 2024 for a January 2024 cohort buyer produces exactly month number 1, regardless of whether the transaction occurred on February 1 or February 28. We aggregate distinct active users per cohort and month offset. Using calendar component arithmetic guarantees that every transaction falls strictly into its natural calendar interval. This structural normalization allows the subsequent pivot query to assemble a deterministic matrix where Month 0, Month 1, and Month 2 reflect true monthly recurrence.

    SQLStep 4
    WITH cohorts AS (
      SELECT customer_id, DATE_TRUNC('month', MIN(order_date)) AS cohort_month
      FROM orders
      GROUP BY customer_id
    ),
    activity AS (
      SELECT
        o.customer_id,
        c.cohort_month,
        (DATE_PART('year', o.order_date) - DATE_PART('year', c.cohort_month)) * 12 +
        (DATE_PART('month', o.order_date) - DATE_PART('month', c.cohort_month)) AS month_number
      FROM orders o
      JOIN cohorts c ON o.customer_id = c.customer_id
    )
    SELECT cohort_month, month_number, COUNT(DISTINCT customer_id) AS active_users
    FROM activity
    GROUP BY cohort_month, month_number
    ORDER BY cohort_month, month_number;
    cohort_month        | month_number | active_users
    --------------------+--------------+-------------
    2024-01-01 00:00:00 | 0            | 2
    2024-01-01 00:00:00 | 1            | 1
    2024-01-01 00:00:00 | 2            | 2
    2024-02-01 00:00:00 | 0            | 1
    2024-02-01 00:00:00 | 1            | 1
    Common mistake

    Failing to multiply the year difference by 12, causing December 2023 and January 2024 to calculate as negative 11 months.

    How you will know

    Active user counts show 2 users in January month 0, 1 in month 1, and 2 in month 2.

  5. Pivot retention matrix with conditional aggregation

    In the final step, we pivot our normalized activity rows into an executive cohort matrix using conditional CASE WHEN aggregations. We join cohort sizes to normalize active counts into retention percentages. In Month 0, 100 percent of acquired customers are active. For the January cohort of 2 buyers, 1 customer returned in Month 1 (50.0 percent) and both returned in Month 2 (100.0 percent). This triangular matrix provides immediate executive visibility into customer repeat purchase velocity. By evaluating active buyer counts through conditional aggregation within a single query pass, we eliminate costly self-joins and produce an optimized reporting shape that is easy to chart on executive growth dashboards without requiring client-side spreadsheet manipulation.

    SQLStep 5
    WITH cohorts AS (
      SELECT customer_id, DATE_TRUNC('month', MIN(order_date)) AS cohort_month
      FROM orders
      GROUP BY customer_id
    ),
    cohort_sizes AS (
      SELECT cohort_month, COUNT(DISTINCT customer_id) AS cohort_size
      FROM cohorts
      GROUP BY cohort_month
    ),
    activity AS (
      SELECT
        o.customer_id,
        c.cohort_month,
        (DATE_PART('year', o.order_date) - DATE_PART('year', c.cohort_month)) * 12 +
        (DATE_PART('month', o.order_date) - DATE_PART('month', c.cohort_month)) AS month_number
      FROM orders o
      JOIN cohorts c ON o.customer_id = c.customer_id
    )
    SELECT
      cs.cohort_month,
      cs.cohort_size,
      ROUND(100.0 * COUNT(DISTINCT CASE WHEN a.month_number = 0 THEN a.customer_id END) / cs.cohort_size, 1) AS m0_pct,
      ROUND(100.0 * COUNT(DISTINCT CASE WHEN a.month_number = 1 THEN a.customer_id END) / cs.cohort_size, 1) AS m1_pct,
      ROUND(100.0 * COUNT(DISTINCT CASE WHEN a.month_number = 2 THEN a.customer_id END) / cs.cohort_size, 1) AS m2_pct
    FROM cohort_sizes cs
    LEFT JOIN activity a ON cs.cohort_month = a.cohort_month
    GROUP BY cs.cohort_month, cs.cohort_size
    ORDER BY cs.cohort_month;
    cohort_month        | cohort_size | m0_pct | m1_pct | m2_pct
    --------------------+-------------+--------+--------+-------
    2024-01-01 00:00:00 | 2           | 100    | 50     | 100
    2024-02-01 00:00:00 | 1           | 100    | 100    | 0
    Common mistake

    Dividing by total table rows rather than the specific cohort base size cs.cohort_size, diluting true cohort retention rates.

    How you will know

    The result displays cohort_month, cohort_size, m0_pct, m1_pct, and m2_pct in clean tabular format.

Build it yourself

Write your solution below, run it in your browser, and pass every check.

DuckDB SQL (In-Browser)Ready to run
Loading editor…
Press Run Code to evaluate
Query executes without SQL errors
Result contains columns: cohort_month, cohort_size, m0_pct, m1_pct, m2_pct
Exactly 2 monthly acquisition cohorts returned
January cohort size is 2
January Month 0 retention is 100.0%
January Month 1 retention is 50.0%
January Month 2 retention is 100.0%

What the result means

Interpretation

The cohort matrix reveals strong customer stickiness for the January cohort, where 50 percent of acquired users purchased again in Month 1 and 100 percent purchased in Month 2. Rather than experiencing steep drop-offs, early buyers develop increased purchase frequency as product familiarity grows. This proves that high customer acquisition costs in January were justified by repeat lifetime value.

The message you would send

Hi Marketing Team, the completed SQL cohort model confirms that 100 percent of our January cohort customers returned to make repeat purchases by Month 2. We recommend maintaining current paid acquisition budgets for high-intent search campaigns while scaling back affiliate discounts that exhibit lower repeat rates.

Put it on your CV

Resume or LinkedIn bullet

OutputCopy this line
Engineered an enterprise customer cohort retention matrix in SQL using CTEs, date truncation, and conditional aggregation in DuckDB.

README for GitHub

OutputREADME.md
# User Retention and Churn Cohort Matrix

A production SQL cohort analysis pipeline tracking customer retention and churn dynamics across monthly acquisition cohorts.

## Features
- Customer lifetime acquisition month identification via window and aggregate MIN
- Exact calendar month interval calculations resilient to varying month lengths
- Horizontal retention pivoting using conditional CASE WHEN aggregations
- Executive triangular cohort matrix suitable for growth and finance reviews
Stretch goal

Extend the query to compute cumulative average revenue per user (ARPU) across Month 0, Month 1, and Month 2 to evaluate acquisition payback windows.

Keep going