Unsold Product Categories

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

Domain
SQL
Difficulty
medium
Seniority
L4
Asked in
General Assembly

Problem

Dead inventory sits in the products catalog without ever appearing in transactions, and the merchandising team's audit sizes it one category at a time. Report what percentage of each category's products are dead.

Dead inventory inflating storage costs.

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 11 rows
categoryunsold_pct
Automotive55
Beauty55
Collectibles0
Electronics50
Sports61.111111111111114

Worked solution and explanation

Why this problem exists in real interviews

Beneath the dead-inventory story this is a per-category anti-join: for each category, find the products with NO matching transaction, then express that count as a share of the whole category. Anyone can spot the LEFT JOIN. The catch lives in the denominator. A product that sold sits on thousands of transaction rows, so joining products straight to the 60M-row transactions table fans every sold product out into a pile of duplicate rows. Count product_id plainly and you are counting rows, not products: the category total balloons and the unsold share collapses toward zero. COUNT(DISTINCT p.product_id) is what pulls the true product count back out. Miss it and every dead-inventory category reads as perfectly healthy.


Break down the requirements

Step 1: Keep every product with a left join

LEFT JOIN products to transactions on product_id so every product survives, including the ones that never sold. An inner join would silently discard exactly the products you are trying to find.

Step 2: Count the unsold products

A product that never sold has no match, so every transaction column on the joined side comes back NULL. SUM(CASE WHEN t.transaction_id IS NULL THEN 1 ELSE 0 END) counts those unsold products, and because an unmatched product produces exactly one NULL row, the fan-out from sold products never touches this count.

Step 3: Divide by the true product count

Divide by COUNT(DISTINCT p.product_id), not a plain COUNT. The direct join multiplies each sold product by its transaction count, so only DISTINCT recovers the real number of products in the category. Multiply the numerator by 100.0 to force float division and avoid integer-truncated zeros.


The solution

Left join with conditional aggregation for the unsold ratio
SELECT
    p.category,
    SUM(CASE WHEN t.transaction_id IS NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(DISTINCT p.product_id) AS unsold_pct
FROM products p
LEFT JOIN transactions t ON p.product_id = t.product_id
GROUP BY p.category

Common follow-up questions