N015-M2 Tier 2 · Core SQL · medium ecommerce · Brightlane

Return the product ID and average unit price for every product whose mean line price exceeds `$300`

Part of HAVING in SQL

The problem

Brightlane's procurement team is reviewing pricing patterns for products that appear on high-value order lines.

Write a query to return the product ID and average unit price for every product whose mean line price exceeds $300.

Assumptions:

  • The order_items table contains one row per product per order.
  • The average is taken across all of the product's line-item unit_price values.
  • The threshold (> $300) applies to the per-product average.

Output:

  • One row per qualifying product, with columns product_id and avg_unit_price.
Schema · ecommerce 5 tables
categories
id integer
name text
parent_id? integer
products
id integer
name text
category_id integer
price numeric
stock_qty integer
attributes? jsonb
order_items
id integer
order_id integer
product_id integer
quantity integer
unit_price numeric
customers
id integer
name text
email text
city? text
country text
created_at timestamptz
is_active boolean
orders
id integer
customer_id integer
ordered_at timestamptz
status text
total_amount numeric

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution Try it yourself first
Solution query
SELECT
  product_id,
  AVG(unit_price) AS avg_unit_price
FROM
  order_items
GROUP BY
  product_id
HAVING
  AVG(unit_price) > 300

The shape

GROUP BY product_id builds the per-product line set; AVG(unit_price) computes each product's mean line price; HAVING AVG(unit_price) > 300 keeps the ones above $300. Product 6 lands at 1999, product 9 at 1499, down to product 21 at 349.

Clause by clause

  • SELECT product_id, AVG(unit_price) AS avg_unit_price returns the grouping column with its per-product average price. AVG reads the unit_price value from each of the product's lines and returns the mean.
  • FROM order_items is the source set: one row per (order, product) line, each with its own unit_price on that order.
  • GROUP BY product_id partitions those lines into one group per product. After this clause, each row in the working set represents one product with its average line price attached.
  • HAVING AVG(unit_price) > 300 filters those product rows by the average. The aggregate expression is repeated rather than referenced by the avg_unit_price alias, because aliases don't exist at the point HAVING runs.

Why this and not WHERE unit_price > 300

The two filters answer different questions. WHERE unit_price > 300 keeps only individual lines priced above $300 and then averages those. A product with three lines at $310, $320, and $50 would be reported with an average around $315, because the $50 line never reaches the aggregation. HAVING AVG(unit_price) > 300 keeps every line, computes the honest mean across all of them, and only then checks the threshold. The same product would correctly average $226.67 and drop out.

The trap

The trap is reaching for WHERE because unit_price looks like a row-level thing. It is a row-level column, but the threshold is on the per-product average, which is a group-level value. WHERE silently filters the inputs to the average rather than the average itself, and the answer comes back as a plausible-looking number. The rule: a condition on an aggregate goes in HAVING, even when the underlying column is plain row data.

You practiced filtering with an AVG-based threshold in HAVING. The same shape applies to any aggregate — SUM, COUNT, MIN, MAX, AVG — the only constraint is that the filtering expression must be an aggregate, not a raw column reference.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.

Practice, feedback, mastery, review. That's the loop that turns reading into real skill.

Start free

No account, no credit card. Start solving in under a minute.