The request

From Megan Collins, Head of Sales
Your goal
Step 1 of 3. For September to December 2025, return month (text 'YYYY-MM', use strftime(order_date, '%Y-%m')), orders (number of distinct completed orders with at least one counted line), net_revenue (rounded to 2) and avg_order_value (net revenue / orders, rounded to 2). Sort by month ascending.
- Read from the
orderstablenot done yet - Read from the
order_itemstablenot done yet - Show exactly
monthordersnet_revenueavg_order_valuenot done yet
Step 2 of 3. Compare the holiday season (November 1 to December 31) of 2024 and 2025 by category. Return category, rev_2024 and rev_2025 (net revenue in each season, rounded to 2) and yoy_change_pct ((rev_2025 - rev_2024) * 100 / rev_2024, from unrounded sums, rounded to 2). Sort by yoy_change_pct descending.
- Read from the
productstablenot done yet - Read from the
orderstablenot done yet - Show exactly
categoryrev_2024rev_2025yoy_change_pctnot done yet
Step 3 of 3. For 2025, return channel, nov_revenue and dec_revenue (net revenue in November and in December, rounded to 2) and mom_change_pct ((dec - nov) * 100 / nov, from unrounded sums, rounded to 2). Sort by channel ascending.
- Read from the
orderstablenot done yet - Read from the
order_itemstablenot done yet - Show exactly
channelnov_revenuedec_revenuemom_change_pctnot done yet
This case unlocks when you finish:
The ticket and lesson are open to read now. Solve every level in those chapters, in any order, 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
| product_id | name | category | subcategory | unit_price |
|---|---|---|---|---|
| 101 | Vale Velvet Loveseat | Furniture | Sofas | 1659.00 |
| 102 | Alder Oak Coffee Table | Furniture | Tables | 919.00 |
| 103 | Orchard Rattan Dining Chair | Furniture | Chairs | 789.00 |
| 104 | Upland Oak Bookcase | Furniture | Storage | 429.00 |
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 Megan 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 |
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 |
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 |
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
How an analyst reads this ticket
Megan’s message contains three questions at three different cuts of the data: time, category and channel. Before writing any SQL, pin down the one definition all three share, because a review in which the monthly total and the category total disagree is worse than no review:
- Revenue = net line revenue:
quantity * unit_price * (1 - discount_pct / 100.0).discount_pctis stored in percentage points (15 means 15%), so dividing by 100 is not optional. - Which orders:
completedonly. Cancelled orders never shipped, refunded orders were paid back and pending orders may still fail. Including them inflates December the most, because pending orders cluster in the last days of the year. - Which lines:
quantity > 0. A handful of lines have zero or negative quantities from data entry errors. - Not revenue:
shipping_fee. It lives onorders, one per order, and has its own question.
Write the definition into the top of your notebook or query file. Then each step is just a different GROUP BY on the same base.
Two tools this case uses
Month labels with strftime. strftime(date, format) turns a date or timestamp into text in
the shape you ask for. %Y is the four-digit year and %m the two-digit month:
SELECT order_id, order_date, strftime(order_date, '%Y-%m') AS month
FROM orders
ORDER BY order_id
LIMIT 2;
| order_id | order_date | month |
|---|---|---|
| 1001 | 2023-01-01 20:51:19 | 2023-01 |
| 1002 | 2023-01-02 07:58:46 | 2023-01 |
Because '2025-09' sorts before '2025-10' as text, ORDER BY month puts these labels in calendar
order. You can GROUP BY the alias month in DuckDB.
Naming a step with WITH. A CTE (common table expression) gives a query a name so the next
query can read from it like a table. It is handy when you want to aggregate first and work with the
totals after:
WITH by_month AS (
SELECT strftime(order_date, '%Y-%m') AS month, COUNT(*) AS orders
FROM orders
WHERE status = 'completed'
GROUP BY month
)
SELECT month, orders
FROM by_month
WHERE orders > 600
ORDER BY month;
The part in brackets runs first and produces a small table called by_month, one row per month;
the SELECT after it filters that table and returns three busy months: 2024-12 (629), 2025-11 (651)
and 2025-12 (634). Steps 2 and 3 use exactly this: a CTE holds the unrounded revenue per period,
and the outer SELECT rounds and computes the growth.
Step 1: the season against the autumn
The base is order_items joined to orders. Each line has exactly one order, so the join adds the
status and date without adding rows. Counting orders is where the grain bites: after the join there
is one row per line, so COUNT(*) counts lines, not orders. COUNT(DISTINCT o.order_id) is the fix,
and average order value is revenue divided by that distinct count.
Grouping by strftime(o.order_date, '%Y-%m') gives sortable text months. Filter the date with a
half-open range (>= '2025-09-01' AND < '2026-01-01') so the last evening of December, a timestamp
like 2025-12-31 21:14, is not lost.
Step 2: year over year by category
Comparing two periods side by side is conditional aggregation: one SUM(CASE WHEN ... END) per
period. Rows outside both seasons give NULL inside the CASE and SUM ignores them. A CTE keeps
the raw sums unrounded so the growth percentage is calculated on exact values; rounding first and
then dividing can move the last decimal.
Holiday growth is a year-over-year question, not a month-over-month one: comparing December with November tells you about the calendar, comparing December 2025 with December 2024 tells you about the business.
Step 3: did December beat November?
Same pattern, two months, grouped by channel. Expect December to be close to November: Black Friday and Cyber Monday land in late November, while December has the HOLIDAY20 push and a dead Christmas Eve and Day. Which channel grew is the useful part of the answer.
SQL and pandas side by side
| Idea | SQL | pandas |
|---|---|---|
| line revenue | expression inside SUM |
a new column with assign |
| month label | strftime(order_date, '%Y-%m') |
order_date.dt.strftime('%Y-%m') |
| distinct orders | COUNT(DISTINCT order_id) |
("order_id", "nunique") |
| two periods as columns | SUM(CASE WHEN ...) |
filter, then pivot_table |
Pitfalls
- Joining
orderstoorder_itemsand then summingshipping_feecounts each fee once per line. BETWEEN '2025-12-01' AND '2025-12-31'on a timestamp stops at midnight on the 31st.- Integer division is not a risk here because the expression is already a decimal, but multiplying
by
100.0rather than100keeps percentage maths safe everywhere.
Lantern lit!
Practised: date filterconditional aggregationcount distinctyoymom
- Solved
- No hints
- First run
