The request

From Jessica Turner, Merchandising Manager
Your goal
Using completed orders placed from 2025-04-01 up to (not including) 2025-07-01 and lines with quantity > 0, compute units sold per product (sum of quantity). Return every product whose rank within its category is 3 or better, where tied products share a rank (RANK by units descending), with: category, product_id, name, units and units_rank. Sort by category, then units_rank, then product_id.
- Read from the
productstablenot done yet - Read from the
orderstablenot done yet - Show exactly
categoryproduct_idnameunitsunits_ranknot done yet
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.
What Jessica gets, and why
Tables
Harbor & Pine store data, ready to query.
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 |
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 |
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
“Top 3 per category” sounds unambiguous until two products tie for third. Then you have to decide: exactly three rows per category, or everyone who earned a top-3 spot? That is a business decision, and the ranking function is how you encode it.
Two definitions, two functions
- Exactly N rows:
ROW_NUMBER()with a tie-breaker (ORDER BY units DESC, product_id). Good for a homepage carousel with three slots. - Everyone in the top N ranks:
RANK(). Tied products share a rank, so a category can return more than N rows. Good for a report where dropping a product would be unfair or misleading.
DENSE_RANK() is a third option: after a tie it does not skip numbers, so “top 3 dense ranks” can
return many more products. Use it when you mean “top 3 distinct values”.
Worked example: Lighting in spring 2025
From April to June 2025 (completed orders, positive quantities), Lighting looks like this:
| product_id | name | units | RANK | ROW_NUMBER |
|---|---|---|---|---|
| 122 | Monroe Glass Desk Lamp | 84 | 1 | 1 |
| 135 | Ridge Ceramic Chandelier | 44 | 2 | 2 |
| 127 | Dune Black Metal Pendant Light | 38 | 3 | 3 |
| 131 | Ashford Linen Pendant Light | 38 | 3 | 4 |
With ROW_NUMBER the Ashford pendant disappears only because its id is larger. With RANK both
pendants are third, and the full answer has 19 rows: three for five categories and four for
Lighting.
The shape
Aggregate, rank in a CTE, filter outside:
ranked AS (
SELECT *,
RANK() OVER (PARTITION BY category ORDER BY units DESC) AS units_rank
FROM product_units
)
SELECT ... FROM ranked WHERE units_rank <= 3
The outer filter is required: WHERE runs before window functions, so WHERE RANK() OVER (...) <= 3
is an error.
QUALIFY
DuckDB, Snowflake, BigQuery and Databricks accept QUALIFY, a filter that runs after window
functions, so the extra CTE is not needed:
SELECT category, product_id, name, units,
RANK() OVER (PARTITION BY category ORDER BY units DESC) AS units_rank
FROM product_units
QUALIFY units_rank <= 3;
Postgres and MySQL do not support it; the CTE works everywhere.
Common mistakes
- Ranking without PARTITION BY. You get the top 3 of the whole store, not of each category.
- ROW_NUMBER without a tie-breaker. The result can change between runs when values tie.
- Ranking before filtering the period. Filter the dates in the aggregate CTE, then rank.
In SQL Server
No QUALIFY, so keep the CTE and filter outside. TOP (3) WITH TIES only works for the whole
result, not per group.
SELECT * FROM ranked WHERE units_rank <= 3 ORDER BY category, units_rank, product_id; Lantern lit!
Practised: top n per groupranktiesqualifycte
- Solved
- No hints
- First run
