N025-H1 Tier 2 · Core SQL · hard ecommerce · Brightlane

Return the name of every such product

Part of Subqueries in WHERE (IN, EXISTS, ANY, ALL) in SQL

The problem

Brightlane's navigation team wants to identify products that belong to a leaf category — a category that is not itself the parent of any other category.

Write a query to return the name of every such product.

Assumptions:

  • The products table contains every product in the catalogue.
  • The categories table contains every defined category; parent_id is the parent reference. A category is a parent if its id appears as some other category's parent_id.
  • Some categories are top-level — their parent_id is missing.

Output:

  • One row per leaf-category product, with a single column name.
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
  name
FROM
  products
WHERE
  category_id NOT IN (
    SELECT
      parent_id
    FROM
      categories
    WHERE
      parent_id IS NOT NULL
  )

The shape

A leaf category is one whose id does not appear as any other category's parent_id. So the products that belong to leaf categories are the ones whose category_id is not in the set of parent_id values drawn from categories. The IS NOT NULL inside the subquery is what makes the NOT IN test actually return rows. Strip it out and the result silently collapses to zero.

Clause by clause

  • SELECT name FROM products reads every product in the catalogue. The filter keeps only the products whose category_id isn't a parent of some other category.
  • SELECT parent_id FROM categories WHERE parent_id IS NOT NULL is the inner subquery. It collects every non-null parent_id across the categories table. A top-level category has parent_id = NULL, so the IS NOT NULL filter ensures it contributes nothing to the set. The result is a clean set of category ids referenced as a parent by at least one other category.
  • WHERE category_id NOT IN (...) is the outer membership test, negated. For each product, PostgreSQL checks whether the product's category_id appears in the parent-id set. If it does, the product belongs to a branch category and the row drops. If it doesn't, the category is a leaf and the product name comes back.

Why this and not bare NOT IN

The natural first draft is WHERE category_id NOT IN (SELECT parent_id FROM categories), without the IS NOT NULL filter. It looks right and runs cleanly. It also returns zero rows, every time, no matter the data.

The reason is three-valued logic. Top-level categories have parent_id = NULL, so the inner subquery returns a set that includes NULL alongside the real parent ids. For each outer product, PostgreSQL has to evaluate category_id <> NULL as part of the NOT IN test, and comparisons with NULL produce NULL. The whole test collapses to NULL for every product, the WHERE treats NULL as falsy, and no row passes.

The trap

NOT IN is the silent-zero-rows operator. The query runs, returns an empty result, and there's no error to indicate that the NULL in the subquery is what killed it. Any time NOT IN runs against a subquery whose column can be nullable, pre-filter with IS NOT NULL, or switch the negation to NOT EXISTS. The defensive filter is cheap; the silent failure is expensive.

You practiced the defensive IS NOT NULL check inside a NOT IN subquery. The recurring rule: NOT IN propagates NULL from the inner result into the outer comparison; pre-removing the missing values is what keeps the outer condition truth-valued.

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.