Products Without Sales

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

Domain
SQL
Difficulty
easy
Seniority
L3
Asked in
Natera

Problem

Which products have never appeared in a transaction? Return the product ID and product name.

Listed but never sold.

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

transactions

Columns: transaction_id INTEGER, user_id INTEGER, product_id INTEGER, quantity INTEGER, total_amount REAL, transaction_date TEXT

transactions: 10 of 200 rows
transaction_iduser_idproduct_idquantitytotal_amounttransaction_date
10671971001223.462026-02-02
11342941050336.932026-03-03
12013911099450.42026-04-04
12684881148563.872026-05-05
13355851197002026-06-06
14026821246290.812026-07-07
146977912953104.282026-08-08
153687613444117.752026-09-09
160397313935-452026-01-10
1670107014421144.692026-02-11

Expected output

First 5 of 109 rows
product_idproduct_name
1491Basic Device 11X
2030Deluxe Bundle 22X
2569Pro Unit 33X
3108Ultra Tool 44X
3647Essential Set 55X

Worked solution and explanation

This is a set difference wearing a catalog costume

Strip the business framing and this is products MINUS the product_ids that appear in transactions: a pure anti-join. The skill being probed is whether you can assert NON-existence, not existence. Almost everyone reaches for a join between the two tables, and that instinct is exactly the trap. A positive join can only surface products that DID sell, so inverting it in your head does not invert the SQL. The correct move is LEFT JOIN from products and keep only the rows where the transaction side failed to match. Get the direction wrong and you return the mirror image of the answer: every product that HAS sold. Get the IS NULL column wrong and you leak false positives.

Why the obvious join betrays you

The instinct (wrong)
INNER JOIN products to transactions returns only products that matched a sale. There is no WHERE clause you can bolt on to make an inner join emit the rows that were never there to begin with: an unmatched product simply produces zero rows. Candidates who start here end up computing the exact opposite set, the 91 products that sold.
The anti-join (right)
LEFT JOIN keeps every product (all 200 in the catalog) and pads unmatched ones with NULLs on the transaction side. Filtering WHERE t.transaction_id IS NULL keeps precisely the 109 products for which no transaction row existed. That is the set difference expressed directly.

Building it

Step 1: Anchor on products with a LEFT JOIN

products goes on the left so every catalog row survives the join regardless of sales. If you write it as transactions LEFT JOIN products you are back to only ever seeing rows that have a transaction, which cannot express never-sold.

Step 2: Match on the shared key

ON p.product_id = t.product_id is the linkage. Matched products get one or more transaction rows attached; unmatched products get a single row with the entire transaction side NULL.

Step 3: Keep only the misses, on a non-null key

WHERE t.transaction_id IS NULL selects the padded rows. transaction_id is the transactions primary key, so it is never null in a real match, that is what makes the IS NULL test a reliable never-matched signal rather than a coincidence of missing data.

Left-join null filter for products without sales
SELECT p.product_id, p.product_name
FROM products p
LEFT JOIN transactions t ON p.product_id = t.product_id
WHERE t.transaction_id IS NULL

Common follow-up questions