The request

From Priya Shah, Head of Data
Your goal
Using completed orders only, assign each customer to cohort_month = the month (first day, as a DATE) of their first completed order. A customer is active in a month if they have at least one completed order in it. For cohorts from 2025-01-01 to 2025-06-01, return one row per cohort and month_offset 0, 1, 2 and 3 (month_offset = whole calendar months between cohort_month and the active month) with: cohort_month, month_offset, active_customers (cohort customers active in that month) and retention_pct (active_customers / the cohort's customer count * 100, rounded to 1). Sort by cohort_month, then month_offset.
- Read from the
orderstablenot done yet - Show exactly
cohort_monthmonth_offsetactive_customersretention_pctnot done yet - 24 rows, sorted as askednot done yet
Your query
Loading the editor…
Reference solution
| order_id | customer_id | order_date | status | channel |
|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace |
Mote checks your result
Press Run. Each row that matches the goal becomes a firefly in your lantern.
What Priya gets, and why
Tables
Harbor & Pine store data, ready to query.
orders12,000 rows · 7 columns
order_idintegercustomer_idintegerorder_datetimestampstatusvarcharchannelvarcharshipping_feedecimalcoupon_codevarchar
| order_id | customer_id | order_date | status | channel | shipping_fee | coupon_code |
|---|---|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web | 0.00 | NULL |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web | 0.00 | NULL |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace | 4.99 | WELCOME10 |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace | 4.99 | WELCOME10 |
| 1005 | 82 | 2023-01-02 11:43:36 | completed | web | 0.00 | WELCOME10 |
customers2,018 rows · 9 columns
customer_idintegerfirst_namevarcharlast_namevarcharemailvarcharcityvarcharstatevarcharsignup_datedatesegmentvarcharreferral_sourcevarchar
| customer_id | first_name | last_name | city | state | signup_date | segment | referral_source | |
|---|---|---|---|---|---|---|---|---|
| 1 | Owen | Hudson | owen.hudson@icloud.com | San Francisco | CA | 2022-06-02 | consumer | search |
| 2 | Douglas | Mendoza | dmendoza69@gmail.com | Atlanta | GA | 2022-06-02 | consumer | NULL |
| 3 | Henry | Brooks | hbrooks@lakesidecafe.com | Los Angeles | CA | 2022-06-04 | business | NULL |
| 4 | Brian | Graham | bgraham@bluecedarhomes.com | Seattle | WA | 2022-06-06 | business | |
| 5 | Kyle | Jimenez | kjimenez27@yahoo.com | Indianapolis | IN | 2022-06-07 | consumer | NULL |
products120 rows · 8 columns
product_idintegernamevarcharcategoryvarcharsubcategoryvarcharunit_pricedecimalunit_costdecimallaunched_ondateis_activeboolean
| product_id | name | category | subcategory | unit_price | unit_cost | launched_on | is_active |
|---|---|---|---|---|---|---|---|
| 101 | Vale Velvet Loveseat | Furniture | Sofas | 1659.00 | NULL | 2023-09-11 | true |
| 102 | Alder Oak Coffee Table | Furniture | Tables | 919.00 | 503.50 | 2024-05-16 | true |
| 103 | Orchard Rattan Dining Chair | Furniture | Chairs | 789.00 | 429.87 | 2023-05-27 | true |
| 104 | Upland Oak Bookcase | Furniture | Storage | 429.00 | 214.70 | 2022-12-20 | true |
| 105 | Avery Leather Bed Frame | Furniture | Beds | 449.00 | 232.70 | 2021-08-31 | true |
order_items30,338 rows · 6 columns
order_item_idintegerorder_idintegerproduct_idintegerquantityintegerunit_pricedecimaldiscount_pctdecimal
| order_item_id | order_id | product_id | quantity | unit_price | discount_pct |
|---|---|---|---|---|---|
| 1 | 1001 | 121 | 9 | 31.99 | 0.00 |
| 2 | 1001 | 154 | 3 | 11.99 | 0.00 |
| 3 | 1002 | 154 | 4 | 11.99 | 0.00 |
| 4 | 1002 | 194 | 1 | 149.00 | 0.00 |
| 5 | 1002 | 193 | 2 | 19.99 | 0.00 |
returns1,673 rows · 5 columns
return_idintegerorder_item_idintegerreturned_ondatereasonvarcharrefund_amountdecimal
| return_id | order_item_id | returned_on | reason | refund_amount |
|---|---|---|---|---|
| 1 | 14 | 2023-01-10 | damaged | 83.69 |
| 2 | 42 | 2023-01-10 | wrong_item | 19.99 |
| 3 | 105 | 2023-01-14 | wrong_item | 10.99 |
| 4 | 24 | 2023-01-16 | changed_mind | 26.99 |
| 5 | 126 | 2023-01-16 | late | 20.99 |
employees60 rows · 8 columns
employee_idintegerfirst_namevarcharlast_namevarchardepartmentvarchartitlevarcharmanager_idintegerhire_datedatesalaryinteger
| employee_id | first_name | last_name | department | title | manager_id | hire_date | salary |
|---|---|---|---|---|---|---|---|
| 1 | Catherine | Hale | Executive | Chief Executive Officer | NULL | 2016-03-01 | 285000 |
| 2 | Robert | Kim | Executive | Chief Operating Officer | 1 | 2017-01-09 | 235000 |
| 3 | Megan | Collins | Sales | Head of Sales | 1 | 2018-04-16 | 172000 |
| 4 | Chloe | Gutierrez | Sales | Account Executive | 3 | 2020-01-21 | 72000 |
| 5 | Frank | Bell | Sales | Account Executive | 3 | 2023-05-21 | 73000 |
web_sessions40,000 rows · 7 columns
session_idintegercustomer_idintegerstarted_attimestampdevicevarcharsourcevarcharpages_viewedintegerconvertedboolean
| session_id | customer_id | started_at | device | source | pages_viewed | converted |
|---|---|---|---|---|---|---|
| 1 | NULL | 2023-01-01 23:02:24 | mobile | paid | 1 | false |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid | 1 | false |
| 3 | NULL | 2023-01-01 16:55:46 | desktop | direct | 7 | false |
| 4 | NULL | 2023-01-01 12:43:28 | mobile | organic | 3 | false |
| 5 | NULL | 2023-01-01 13:06:21 | mobile | social | 2 | false |
marketing_spend3,276 rows · 3 columns
spend_datedatechannelvarcharamountdecimal
| spend_date | channel | amount |
|---|---|---|
| 2023-01-01 | paid | 492.99 |
| 2023-01-01 | 37.38 | |
| 2023-01-01 | social | 200.53 |
| 2023-01-02 | paid | 709.13 |
| 2023-01-02 | 64.13 |
The lesson
The month-1 retention you computed in the window functions boss is one column of a bigger picture. A retention grid shows, for each cohort, how many customers were active 0, 1, 2, 3 months after their first order. Reading down a column compares cohorts; reading across a row shows how one cohort decays.
Month offsets
The grid’s key column is the offset: how many calendar months separate an active month from the
cohort month. DATEDIFF('month', start, end) counts month boundaries crossed, so
DATEDIFF('month', DATE '2025-01-01', DATE '2025-03-01') is 2. Because both dates are first-of-month,
that is exactly “two months later”.
DATEDIFF('month', cohort_month, order_month) AS month_offset
Offset 0 is the cohort month itself. Every cohort customer is active there by definition, which makes the offset-0 count the cohort size.
Long format first
SQL is good at long tables (one row per cohort and offset) and awkward at wide ones. Produce the long
grid, then pivot it in a spreadsheet or BI tool. If you must pivot in SQL, conditional aggregation
does it: COUNT(*) FILTER (WHERE month_offset = 1) AS m1.
Worked example: the first two 2025 cohorts
| cohort_month | month_offset | active_customers | retention_pct |
|---|---|---|---|
| 2025-01-01 | 0 | 37 | 100.0 |
| 2025-01-01 | 1 | 4 | 10.8 |
| 2025-01-01 | 2 | 3 | 8.1 |
| 2025-01-01 | 3 | 4 | 10.8 |
| 2025-02-01 | 0 | 28 | 100.0 |
| 2025-02-01 | 3 | 7 | 25.0 |
Two things to notice. Retention can go up between offsets: this grid asks “active in that month”, not “came back at least once by then”, so a customer can skip month 2 and return in month 3. And these cohorts are small (28 to 52 customers in the first half of 2025, because most Harbor & Pine customers placed their first order earlier), so one customer moves a cell by 2 to 4 points.
Choosing the cohort range
The June 2025 cohort’s offset 3 is September 2025, which is complete. A cohort from November 2025 has no month 3 yet; including it would show empty or misleadingly low cells. Only show offsets that have fully happened.
Common mistakes
- Counting orders, not customers. Deduplicate to customer-month first.
- Filtering before the cohort window. Find the first order on all history, then filter cohorts.
- Missing zeros. A cohort with no active customers at some offset has no row at all. For a full grid, cross join cohorts with offsets 0 to 3 and LEFT JOIN the counts.
In SQL Server
DATEDIFF(month, cohort_month, order_month) counts month boundaries exactly like DuckDB. Build month
starts with DATEFROMPARTS(YEAR(order_date), MONTH(order_date), 1), and next month with
DATEADD(month, 1, cohort_month).
Lantern lit!
Practised: cohort retentiondatediffmonth offsetctewindow functions
- Solved
- No hints
- First run
