Never-Ordered Products
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
Problem
Which products in the catalog have never been ordered? Show product ID and product name for items with no matching transaction.
In the catalog. Never purchased.
Tables
products
Columns: product_id INTEGER, product_name TEXT, category TEXT, price REAL, rating REAL, in_stock INTEGER
| product_id | product_name | category | price | rating | in_stock |
|---|---|---|---|---|---|
| 1001 | Basic Device 1X | Books | 17.52 | 2.7 | 1 |
| 1050 | Deluxe Bundle 2X | Clothing | 25.05 | 4.4 | 1 |
| 1099 | Pro Unit 3X | Home & Kitchen | 32.58 | 2.1 | 1 |
| 1148 | Ultra Tool 4X | Collectibles | 40.11 | 3.8 | 1 |
| 1197 | Essential Set 5X | Toys | 47.64 | 1.5 | 1 |
| 1246 | Classic System 6X | Beauty | 0 | 3.2 | 1 |
| 1295 | Smart Gadget 7X | Automotive | 62.7 | 4.9 | 0 |
| 1344 | Eco Kit 8X | Garden | 70.23 | 2.6 | 1 |
| 1393 | Turbo Pack 9X | Music | 77.76 | 4.3 | 0 |
| 1442 | Premium Widget 10X | Electronics | 85.29 | 2 | 1 |
transactions
Columns: transaction_id INTEGER, user_id INTEGER, product_id INTEGER, quantity INTEGER, total_amount REAL, transaction_date TEXT
| transaction_id | user_id | product_id | quantity | total_amount | transaction_date |
|---|---|---|---|---|---|
| 1067 | 197 | 1001 | 2 | 23.46 | 2026-02-02 |
| 1134 | 294 | 1050 | 3 | 36.93 | 2026-03-03 |
| 1201 | 391 | 1099 | 4 | 50.4 | 2026-04-04 |
| 1268 | 488 | 1148 | 5 | 63.87 | 2026-05-05 |
| 1335 | 585 | 1197 | 0 | 0 | 2026-06-06 |
| 1402 | 682 | 1246 | 2 | 90.81 | 2026-07-07 |
| 1469 | 779 | 1295 | 3 | 104.28 | 2026-08-08 |
| 1536 | 876 | 1344 | 4 | 117.75 | 2026-09-09 |
| 1603 | 973 | 1393 | 5 | -45 | 2026-01-10 |
| 1670 | 1070 | 1442 | 1 | 144.69 | 2026-02-11 |
Expected output
| product_id | product_name |
|---|---|
| 1491 | Basic Device 11X |
| 2030 | Deluxe Bundle 22X |
| 2569 | Pro Unit 33X |
| 3108 | Ultra Tool 44X |
| 3647 | Essential Set 55X |
Worked solution and explanation
What this really is
This is an anti-join dressed up as a catalog audit: list the rows of products that have no partner at all in transactions. Anyone can relate the two tables on product_id. What separates candidates is remembering that a plain join only keeps pairs that matched. The products the question is about are gone before any filter runs. Write the inner join and you get the 91 products that have sold, the exact opposite of the 109 that never have.
Building it
Step 1: Keep every product, matched or not
LEFT JOIN transactions t ON p.product_id = t.product_id keeps each row of products even when no transaction points at it. Unmatched products come through once, with every t column set to NULL. A product that sold fans out to one row per sale, which does not matter because those rows are about to be discarded.
Step 2: Test a column a real match always fills
WHERE t.transaction_id IS NULL keeps only the padded rows. Test the right side's key, not a measure like t.quantity or t.total_amount. Transaction 1335 is a voided sale with quantity 0, and it still means the product was ordered.
Step 3: Return the product
Select p.product_id and p.product_name. No dedup is needed: an unmatched product produces exactly one padded row, so each survivor appears once.
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- Inner join
JOIN transactions t ON p.product_id = t.product_idemits a row only where a sale exists. Every never-ordered product is dropped by the join itself, and noWHEREclause can bring it back.- Left join plus a null test
LEFT JOINpads unmatched products withNULLs, andt.transaction_id IS NULLselects exactly those 109 rows.
Common follow-up questions
- Which products sold nothing in 2026? (Tests whether the date condition goes in the `ON` clause. Put in `WHERE`, it compares the padded `NULL` dates and discards every unmatched product.)
- Should a product whose only sale was refunded count as ordered? (Tests reading the business definition: transaction 1603 has a negative `total_amount`.)
- Rewrite it with `NOT EXISTS`. Why is that safer than `NOT IN` here? (Tests null semantics in subquery predicates over a nullable `product_id`.)