The Above Average

A medium SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.

Domain
SQL
Difficulty
medium
Seniority
L4

Problem

The pricing team is flagging products that look overpriced relative to their own category. For every product whose listed price exceeds the average price within its category, return the product name, category, price, and that category's average price.

What counts as good depends on the company you keep.

Tables

products

Columns: product_id INTEGER, product_name TEXT, category TEXT, price REAL, rating REAL, in_stock INTEGER

products: 10 of 200 rows
product_idproduct_namecategorypriceratingin_stock
1001Basic Device 1XBooks17.522.71
1050Deluxe Bundle 2XClothing25.054.41
1099Pro Unit 3XHome & Kitchen32.582.11
1148Ultra Tool 4XCollectibles40.113.81
1197Essential Set 5XToys47.641.51
1246Classic System 6XBeauty03.21
1295Smart Gadget 7XAutomotive62.74.90
1344Eco Kit 8XGarden70.232.61
1393Turbo Pack 9XMusic77.764.30
1442Premium Widget 10XElectronics85.2921

Expected output

First 5 of 96 rows
product_namecategorypriceavg_price
Ultra Tool 14XCollectibles115.4177.75999999999999
Deluxe Widget v1Books941.99857.7549999999999
Pro Set v2Clothing951.26832.4021052631579
Ultra Widget v3Home & Kitchen960.53874.555
Classic Widget v5Toys979.07854.505

Worked solution and explanation

The trap

This is a peer-group comparison dressed up as a pricing audit: every product is judged against its own category, not the catalog. Anyone can compute an average. What separates candidates is getting that average back onto each individual row at the right scope. Reach for one storewide AVG(price) and the bar sits near 897, so 'Ultra Tool 14X' at 115.41 never gets flagged even though its two-item 'Collectibles' category averages 77.76. Meanwhile 'Essential Set v4' gets flagged at 969.80 while sitting under its 'Sports' average. Try WHERE price > AVG(price) instead and the query does not even run, because a row filter cannot see an aggregate.

Building it

Step 1: Compute each category's average once

Collapse products to one row per category with AVG(price) AS avg_price in a CTE named category_avgs. This is the only place rows get folded: eleven categories, eleven averages. AVG skips the NULL prices, and the 0.0 free sample in 'Beauty' counts as a real price, which is what a pricing team means by average.

Step 2: Join every product back to its category

Join products to category_avgs on category. Each product picks up exactly one avg_price because the CTE holds one row per category, so nothing fans out. Every product has a category, so the inner join loses nobody.

Step 3: Filter on the row, strictly

Now p.price > ca.avg_price is an ordinary row predicate, so it belongs in WHERE. Strict greater-than: a product sitting exactly on its category average is not overpriced. A NULL price compares as unknown and drops out on its own.

Step 4: Project four columns, no sort

Return product_name, category, price and avg_price. Nothing in the ask is about order, so there is no ORDER BY for the grader to hold you to.

Per-category average, joined back and filtered
WITH category_avgs AS (
    SELECT category, AVG(price) AS avg_price
    FROM products
    GROUP BY category
)
SELECT
    p.product_name,
    p.category,
    p.price,
    ca.avg_price
FROM products p
JOIN category_avgs ca ON p.category = ca.category
WHERE p.price > ca.avg_price
The storewide threshold
SELECT
    product_name,
    category,
    price,
    (SELECT AVG(price) FROM products) AS avg_price
FROM products
WHERE price > (SELECT AVG(price) FROM products)

Same number of rows, different rows: it drops 'Ultra Tool 14X' and admits 'Essential Set v4', which sits under its own category's average.

Common follow-up questions