The request

From Brian Mitchell, Finance Analyst
Your goal
For completed orders, compute order_value as the sum of quantity * unit_price * (1 - discount_pct / 100.0) over lines with quantity > 0, and count each order's lines with quantity <= 0. Compute one upper fence over all completed orders: Q3 + 1.5 * (Q3 - Q1) of order_value, with Q1 and Q3 from quantile_cont at 0.25 and 0.75. Return one row per channel with: channel, orders (completed orders), high_value_orders (orders with order_value above the fence) and orders_with_bad_lines (orders with at least one line with quantity <= 0).
- Read from the
orderstablenot done yet - Read from the
order_itemstablenot done yet - Show exactly
channelordershigh_value_ordersorders_with_bad_linesnot done yet
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 Brian 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
An outlier is a value far from the rest. Some are errors (a quantity of -16), some are real and important (a business customer furnishing an office). Deleting both kinds hides information, so the standard practice is to flag first and decide later.
Rule-based flags
Some values are impossible by definition. A sold quantity of zero or less is a data-entry error. In this dataset 24 order lines have one: 8 zeros and 16 negatives, from -1 down to -16. A rule flag is a plain condition:
CASE WHEN quantity <= 0 THEN 1 ELSE 0 END AS bad_line
Keep those lines out of revenue, but count them. 19 completed orders contain one.
Statistical flags: the IQR fence
For values that are possible but unusual, compare them to the distribution. The interquartile range (IQR) is Q3 minus Q1, the spread of the middle half of the data. Tukey’s rule flags anything above Q3 + 1.5 × IQR (and below Q1 − 1.5 × IQR, which is under zero for order values).
SELECT
quantile_cont(order_value, 0.25) AS q1,
quantile_cont(order_value, 0.75) AS q3
FROM order_values;
For the 10,849 completed orders, Q1 is $93.99 and Q3 is $659.94, so the fence sits at about $1,508.87.
The median order is $268.96 and the largest is $11,103.27. quantile_cont interpolates between values;
quantile_disc returns an actual value from the data.
Reading the result
926 completed orders, about 8.5%, are above the fence. That is a lot for an “outlier” rule, and it tells you something: order values are heavily right-skewed, with many small orders and a long tail of big furniture and business purchases. The IQR rule was designed for roughly symmetric data. Those orders are real revenue, which is exactly why you flag them rather than delete them. Finance might forecast them separately or model the logarithm of order value instead.
Common mistakes
- Deleting instead of flagging. You can’t undo a
WHEREyou never told anyone about. - Recomputing the fence after removing outliers. The fence moves every time; compute it once on the full data.
- A fence per group by accident. Decide whether the threshold is global or per channel, and make the query say so.
- Mean and standard deviation on skewed data. The big orders inflate both, so a “3 sigma” rule misses what it should catch. Quartiles resist that.
In SQL Server
There is no quantile_cont. PERCENTILE_CONT is a window function there, so it repeats the value on
every row:
SELECT DISTINCT
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY order_value) OVER () AS q1,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY order_value) OVER () AS q3
FROM order_values;
Postgres and Snowflake use percentile_cont(0.75) WITHIN GROUP (ORDER BY order_value) as an aggregate.
Lantern lit!
Practised: outliersiqrquantiledata qualityctedata cleaning
- Solved
- No hints
- First run
