The request

From Catherine Hale, CEO
Your goal
Step 1 of 3. Measure the mess in customers. Return one row with customer_rows (all rows), people (distinct lower(trim(email))), raw_states and clean_states (distinct state before and after upper(trim(...))), raw_cities and clean_cities (the same for city) and missing_city (rows with a NULL city).
- Read from the
customerstablenot done yet - Show exactly
customer_rowspeopleraw_statesclean_statesraw_citiesclean_citiesmissing_citynot done yet - 1 row, sorted as askednot done yet
Step 2 of 3. Rank cleaned states. Return state (cleaned), customers (distinct people in that state, from customers, whether they ordered or not), buyers_2025 (distinct customer_id with a counted line in 2025), revenue_2024 and revenue_2025 (net revenue, rounded to 2), growth_pct ((2025 - 2024) * 100 / 2024 from unrounded sums, rounded to 1) and revenue_per_buyer (2025 revenue / buyers_2025, rounded to 2). Keep the top 10 states by revenue_2025, highest first.
- Read from the
customerstablenot done yet - Read from the
orderstablenot done yet - Show exactly
statecustomersbuyers_2025revenue_2024revenue_2025growth_pctrevenue_per_buyernot done yet
Step 3 of 3. Rank metros for a showroom. Use consumer customers only (segment = 'consumer') with a known city. Return city (cleaned), state (cleaned), buyers_2025, revenue_2024, revenue_2025 (rounded to 2) and growth_pct (rounded to 1), computed as in step 2. Keep the top 8 metros by revenue_2025, highest first.
- Read from the
customerstablenot done yet - Read from the
orderstablenot done yet - Show exactly
citystatebuyers_2025revenue_2024revenue_2025growth_pctnot done yet
This case unlocks when you finish any one of:
The ticket and lesson are open to read now. Solve every level in one of those chapters, in whichever track you prefer, and this case opens.
Step 1 · 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 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.
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 |
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 |
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
What you are checking, and why
Customer addresses are typed by customers, so the same place arrives in several spellings. Before
you group by anything, measure how bad it is. COUNT(DISTINCT ...) on the raw column against the
same count on the cleaned value tells you in one query:
SELECT COUNT(DISTINCT state) AS raw_states,
COUNT(DISTINCT upper(trim(state))) AS clean_states
FROM customers;
trim removes the stray spaces and upper puts every label in one case. People are matched the
same way, on lower(trim(email)), because an email is the closest thing the table has to a
person’s identity: the customer_id changes when someone signs up again.
One SQL detail is worth knowing: COUNT(column) skips NULLs, so COUNT(*) - COUNT(city) is the
number of blank cities. Those 14 customers cannot be placed in a metro and drop out of step 3.
States first, then metros
Step 2 gives the board the familiar state view. It mixes two sources: the customers table for
how many people live in each state, and the order lines for who bought and how much. Each is
aggregated on its own and the two are joined on the cleaned state. Joining first and counting
later would multiply rows.
Step 3 is the answer to the showroom question. It keeps consumers only, because business accounts order online and would not visit, and it ranks metros, because a showroom serves a city:
WHERE o.status = 'completed'
AND i.quantity > 0
AND c.segment = 'consumer'
AND c.city IS NOT NULL
Both steps put the two years side by side with SUM(CASE WHEN yr = ... THEN revenue END) and
compute growth from the unrounded sums.
Reading the result
| city | buyers_2025 | revenue_2025 | growth_pct |
|---|---|---|---|
| NEW YORK | 104 | 96,984.37 | −5.9 |
| LOS ANGELES | 78 | 71,631.90 | 30.7 |
| DALLAS | 58 | 66,362.59 | 17.2 |
New York is the largest and Los Angeles the fastest growing of the leaders. The state table would have pointed at California as a whole, and its top-line growth of 0.3% would have hidden that Los Angeles is growing fast.
Pitfalls
- Cleaning in
GROUP BYbut not in theSELECT(or the other way round) gives an error or splits groups again. Clean once, in a CTE, and use the cleaned column everywhere. upper(trim(city))returnsNEW YORK. That is fine for grouping; tidy the case for the slide.LIMITwithoutORDER BYreturns ten arbitrary states.
Lantern lit!
Practised: data cleaningtrimuppercount distinctdeduplicationcteconditional aggregationyoy
- Solved
- No hints
- First run
