The request

From Laura Bennett, Head of Finance
Your goal
Step 1 of 3. Check the discounts. Join order_items to orders and label each line with coupon = COALESCE(o.coupon_code, 'none'). Return coupon, orders (distinct order ids), lines, min_discount and max_discount (the smallest and largest discount_pct), sorted by coupon. Use all orders, whatever the status.
- Read from the
orderstablenot done yet - Read from the
order_itemstablenot done yet - Show exactly
couponorderslinesmin_discountmax_discountnot done yet
Step 2 of 3. Total each coupon. Use completed orders and lines with quantity > 0. Build a CTE with one row per order: coupon (as in step 1), gross (value before discount) and net (value after it). Return coupon, orders, revenue (sum of net, rounded to 2), aov (revenue per order, rounded to 2), discount_cost (gross minus net, rounded to 2) and revenue_share_pct (the coupon's share of all net revenue, rounded to 1), sorted by coupon.
- Read from the
orderstablenot done yet - Read from the
order_itemstablenot done yet - Show exactly
couponordersrevenueaovdiscount_costrevenue_share_pctnot done yet
Step 3 of 3. Compare like for like. Start from the same order_totals CTE and add day = CAST(o.order_date AS DATE). For each seasonal coupon (BFCM25, HOLIDAY20, SPRING15, SUMMER15), find the days it was used, then look at every completed order placed on those days. Return coupon, coupon_orders and coupon_basket (that coupon's orders and their average value before discount), coupon_aov (their average net value), no_coupon_orders and no_coupon_aov (orders with no code on the same days and their average net value). Round averages to 2, sort by coupon.
- Read from the
orderstablenot done yet - Read from the
order_itemstablenot done yet - Show exactly
couponcoupon_orderscoupon_basketcoupon_aovno_coupon_ordersno_coupon_aovnot done yet
This case unlocks when you finish any one of:
- SQL · Aggregates and GROUP BY
- Python · New columns, groupby and aggregation
- Excel · Conditional totals
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
| 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
| 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 3 · 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 Laura 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 |
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 |
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 |
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
The coupon lives in two places: the code on orders.coupon_code and the discount on every line in
order_items.discount_pct. Every number in this case is built from the second, so first prove it
agrees with the first. One grouped query does it:
SELECT COALESCE(o.coupon_code, 'none') AS coupon,
MIN(i.discount_pct) AS min_discount,
MAX(i.discount_pct) AS max_discount
FROM order_items AS i
JOIN orders AS o ON o.order_id = i.order_id
GROUP BY coupon;
If a code’s minimum and maximum are equal, all its lines carry the same discount. Three other
things to look for in the same result: a code written two ways (welcome10 next to WELCOME10),
orders without a code that still carry a discount, and how many orders have no code at all.
COALESCE matters here: without it the NULL group is easy to filter away by accident.
What each coupon cost
Step 2 rolls the lines up to one row per order first, in a CTE, because a coupon is an order-level choice: the average order value must divide by orders, not by lines. From there the cost sheet is plain aggregation:
- revenue: what customers paid, after the discount;
- aov: revenue per order;
- discount_cost: value before discount minus value after, the money given away;
- revenue_share_pct: each code’s slice of all net revenue, divided by a scalar subquery
(SELECT SUM(net) FROM order_totals)so the denominator is the same total for every row.
Like for like: the same days
Every coupon order averages less than an order without a code. That sounds damning, but the seasonal codes run in particular weeks, and Black Friday baskets are smaller across the board. The fair comparison puts each coupon next to the orders placed on the same days without a code.
The second CTE, coupon_days, lists the days each seasonal code was used. Joining it back to
order_totals on the day gives every order placed on those days, once per coupon. Conditional
aggregation then splits them:
AVG(CASE WHEN t.coupon = d.coupon THEN t.net END) AS coupon_aov,
AVG(CASE WHEN t.coupon = 'none' THEN t.net END) AS no_coupon_aov
WELCOME10 and FREESHIP are left out of this step because they run every day: for them, “the same days” means the whole year.
Reading the result
| coupon | coupon_basket | coupon_aov | no_coupon_aov |
|---|---|---|---|
| BFCM25 | 536.54 | 402.41 | 483.06 |
| HOLIDAY20 | 482.84 | 386.27 | 479.56 |
| SPRING15 | 634.59 | 539.40 | 574.96 |
| SUMMER15 | 671.75 | 570.98 | 658.07 |
The basket before the discount is about the same as, or a little above, a normal order on those days. So the codes did not make people buy much more; they mostly paid less for a similar basket.
Pitfalls
- A coupon order counted once per line inflates
orders; count in the order-level CTE. - Filtering
quantity > 0matters: three coupon lines have a zero or negative quantity. - FREESHIP’s cost is shipping, which is not in these totals. Mention it rather than reporting $0.
Lantern lit!
Practised: data validationcoalescegroup byconditional aggregationctelike for like
- Solved
- No hints
- First run
