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
Google

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

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

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.

Anti-join with `LEFT JOIN` and `IS NULL`
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_id emits a row only where a sale exists. Every never-ordered product is dropped by the join itself, and no WHERE clause can bring it back.
Left join plus a null test
LEFT JOIN pads unmatched products with NULLs, and t.transaction_id IS NULL selects exactly those 109 rows.

Common follow-up questions