The request

From Catherine Hale, Chief Executive Officer
Your goal
Step 1 of 3. Return one row with: customers_2024 (number of distinct 2024 buyers), retained (2024 buyers who are also 2025 buyers), lapsed (2024 buyers who are not 2025 buyers) and retention_pct (retained / customers_2024 * 100, rounded to 1).
- Read from the
orderstablenot done yet - Show exactly
customers_2024retainedlapsedretention_pctnot done yet - 1 rownot done yet
Step 2 of 3. Classify every 2025 buyer and every lapsed 2024 buyer into exactly one status: 'new' (bought in 2025, no completed order before 2025), 'retained' (bought in 2024 and 2025), 'reactivated' (bought in 2025, not in 2024, but before 2024) and 'lapsed' (bought in 2024, not in 2025). Return status and customers (number of customers), one row per status, sorted by customers descending.
- Read from the
customerstablenot done yet - Read from the
orderstablenot done yet - Show exactly
statuscustomersnot done yet
Step 3 of 3. Using the same four statuses, join each 2025 buyer's 2025 net revenue (sum of quantity * unit_price * (1 - discount_pct / 100.0) over lines with quantity > 0 of their completed 2025 orders). Return status, customers, net_revenue_2025 (rounded to 2) and revenue_per_customer (net_revenue_2025 / customers, rounded to 2) for the statuses that have 2025 revenue, sorted by net_revenue_2025 descending.
- Read from the
customerstablenot done yet - Read from the
orderstablenot done yet - Show exactly
statuscustomersnet_revenue_2025revenue_per_customernot done yet
Step 1 · 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.
Step 2 · your query
Loading the editor…
Reference solution
| customer_id | first_name | last_name | city | |
|---|---|---|---|---|
| 1 | Owen | Hudson | owen.hudson@icloud.com | San Francisco |
| 2 | Douglas | Mendoza | dmendoza69@gmail.com | Atlanta |
| 3 | Henry | Brooks | hbrooks@lakesidecafe.com | Los Angeles |
| 4 | Brian | Graham | bgraham@bluecedarhomes.com | Seattle |
Mote checks your result
Press Run. Each row that matches the goal becomes a firefly in your lantern.
Step 3 · your query
Loading the editor…
Reference solution
| customer_id | first_name | last_name | city | |
|---|---|---|---|---|
| 1 | Owen | Hudson | owen.hudson@icloud.com | San Francisco |
| 2 | Douglas | Mendoza | dmendoza69@gmail.com | Atlanta |
| 3 | Henry | Brooks | hbrooks@lakesidecafe.com | Los Angeles |
| 4 | Brian | Graham | bgraham@bluecedarhomes.com | Seattle |
Mote checks your result
Press Run. Each row that matches the goal becomes a firefly in your lantern.
What Catherine 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 |
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 |
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 |
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
This boss turns three set operations into the customer bridge every growth review uses: where this year’s customers came from, and where last year’s went.
Step 1: retained and lapsed
Two buyer lists, one per year, and two comparisons:
- retained = 2024 buyers
INTERSECT2025 buyers - lapsed = 2024 buyers
EXCEPT2025 buyers
The buyer CTEs are one row per order, so count DISTINCT customer_id when you want the size of a list.
The set operations already return distinct ids. At Harbor & Pine, 997 customers bought in 2024; 752 of
them bought again (75.4%) and 245 did not. Retained plus lapsed must equal the 2024 total, which is a
free sanity check.
Step 2: four statuses, one table
2025 had 1,447 buyers. Two of the groups are easy, new and retained. The third is the one people miss: customers who bought before 2024, skipped 2024 and came back in 2025. Calling them “new” overstates acquisition, so they get their own label, reactivated:
SELECT customer_id FROM (
SELECT customer_id FROM buyers_2025
INTERSECT
SELECT customer_id FROM buyers_before_2025
) AS b
EXCEPT
SELECT customer_id FROM buyers_2024
Wrapping the first operation in a subquery makes the order of operations explicit. (INTERSECT binds
more tightly than UNION and EXCEPT in standard SQL, but parentheses are cheaper than remembering.)
The groups do not overlap, so UNION ALL with a status label stacks them, and a GROUP BY counts each:
752 retained, 635 new, 245 lapsed, 60 reactivated. New + retained + reactivated = 1,447, every 2025
buyer exactly once.
Step 3: what each group spent
Now join a set-operation result to real data. Aggregate 2025 revenue to one row per customer first, then join it to the labels. Retained customers bring in about $1.49 million at roughly $1,987 each, new customers about $758,000 at roughly $1,193, and the 60 reactivated customers about $50,000. Lapsed customers have no 2025 revenue, so the inner join drops them, which is what the task asks for.
Common mistakes
- Counting orders instead of customers in step 1 (the starter returns 3,673, the number of completed 2024 orders).
- Putting new and reactivated together, or letting a customer land in two groups. Check that the group totals add up to the yearly buyer counts.
- Joining labels to
order_itemsand counting rows, which turns customers into order lines.
In SQL Server
INTERSECT, EXCEPT and UNION ALL work the same way, and derived tables need an alias as here. The
scalar subqueries in step 1 are fine in T-SQL, but 100.0 * matters more there: with two integer counts,
/ would truncate the percentage to 0.
Lantern lit!
Practised: intersectexceptunion allretentionctejoin
- Solved
- No hints
- First run
