N046-H2 Tier 4 · Advanced · hard ecommerce · Brightlane

Return one row per category group, showing the category ID, ID of the lowest-priced product in that group, the product name, and the price. Sort the final result by `category_id` ascending

Part of DISTINCT ON in SQL

The problem

Brightlane's catalog team needs the lowest-priced product for each category. Products with no category on record form their own group — the lowest-priced uncategorized product should also appear in the result.

Write a query to return one row per category group, showing the category ID, ID of the lowest-priced product in that group, the product name, and the price. Sort the final result by category_id ascending.

Assumptions:

  • The lowest-priced product in a category is the product with the smallest price for that category_id. When two products in the same category share the same price, the product with the smaller id wins.
  • Products with a missing category_id form their own group; the lowest-priced product among that group appears in the result with a missing category_id.
  • The final result is sorted by category_id ascending; the missing-category_id row appears at the end.

Output:

  • One row per category group (each category_id value plus the missing-category_id group), with columns category_id, product_id, name, and price. Sorted by category_id ascending; missing-category_id row last.
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 DISTINCT
  ON (category_id) category_id,
  id AS product_id,
  name,
  price
FROM
  products
ORDER BY
  category_id,
  price,
  id

The shape

A missing category_id is treated like any other distinct value by DISTINCT ON. DISTINCT ON (category_id) keeps one row per distinct category_id, and the missing value is one of those distinct values, so the missing-category products form their own group and get their own row. ORDER BY category_id, price, id picks the cheapest product in each group, with the smaller id winning ties.

Clause by clause

  • SELECT DISTINCT ON (category_id) category_id, id AS product_id, name, price returns the four columns the catalog review needs. DISTINCT ON (category_id) declares one row per distinct category_id value, including the missing value as its own distinct group.
  • FROM products reads the product records.
  • ORDER BY category_id, price, id sorts the products by three ascending keys. The leading category_id ascending satisfies the DISTINCT ON requirement and gives the final result its category-ordered shape; PostgreSQL puts missing values last in an ascending sort by default, which is why the missing-category row appears at the end. The second key, price ascending, makes the cheapest product sit first in each category's group. The third key, id ascending, breaks price ties: when two products in the same category share a price, the one with the smaller id sorts first and wins the per-category pick.

Why this and not ROW_NUMBER

The window-function form behaves the same way on missing values:

SELECT category_id, product_id, name, price
FROM (
  SELECT category_id, id AS product_id, name, price,
    ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price, id) AS rn
  FROM products
) ranked
WHERE rn = 1
ORDER BY category_id

PARTITION BY category_id puts all missing-category rows into the same partition, exactly the way DISTINCT ON (category_id) puts them in the same group. Both forms return the missing-category row as part of the result.

The trap

The instinct on missing values is that they get filtered out or swept aside. With DISTINCT ON they do not. The missing value is its own distinct group, with its own kept row, sorted to wherever the ORDER BY puts it. If the product team had wanted to exclude uncategorized products, the query would have needed an explicit WHERE category_id IS NOT NULL before the per-category pick. Without that filter, the missing-category bucket is in the result by default. The corollary on the sort side: in an ascending ORDER BY, missing values sort last, which is why the uncategorized row appears at the end of the result rather than at the top.

You practiced DISTINCT ON (category_id) over a column that contains missing values — PostgreSQL treats every distinct value (including the missing value itself) as a separate group, so the missing-category_id records form their own bucket.

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.