Units sold per product
The catalogue team wants every product on one sheet, with how many units actually reached customers. Products that never sold must still be on the sheet, with a 0.
Count order_items.quantity only for orders whose status is 'delivered'. A product whose only
sales were cancelled, returned or still shipping has units_sold = 0.
Return one row per product in products:
| column | type | meaning |
|---|---|---|
product_id |
integer | the product |
name |
text | the product name |
units_sold |
integer | total delivered quantity, 0 if none |
Row order doesn’t matter.
Tables: products, order_items, orders.
bitshop
A small online shop for developers. Synthetic data, generated by scripts/gen-datasets.mjs.
Click a table to preview its first 10 rows.
customers 200 rows
one row per customer. About 6% have no email. About 15% never placed an order.
products 40 rows
40 products in 5 categories.
orders 1,000 rows
1,000 orders, 2024–2025. Status is delivered, shipped, cancelled or returned.
order_items 2,423 rows
line items. unit_price can be below the list price (discounts).
price_changes 98 rows
list-price history per product (one row per change, effective from changed_at).
Hints get more specific as you go. Reveal one, try again, then come back.
“Every product, even with no sales” means products is the left side of a LEFT JOIN.
The trap: LEFT JOIN ... WHERE o.status = 'delivered'. For an unsold product, o.status is NULL, the
WHERE throws the row away, and your left join quietly becomes an inner join.
Do the delivered-only sum first, in a subquery grouped by product_id. Then LEFT JOIN products to
that subquery. Nothing on the right side needs filtering any more.
Products with no match get NULL from the subquery. COALESCE(d.units, 0) turns that into 0.
Try one more hint first? The solution is always here, but working it out is what makes it stick.
Approach
Aggregate delivered units per product first, then attach that to the full product list with a
LEFT JOIN.
SELECT p.product_id,
p.name,
COALESCE(d.units, 0) AS units_sold
FROM products p
LEFT JOIN (
SELECT oi.product_id, SUM(oi.quantity) AS units
FROM order_items oi
JOIN orders o ON o.order_id = oi.order_id
WHERE o.status = 'delivered'
GROUP BY oi.product_id
) d ON d.product_id = p.product_id;
Why it works
A LEFT JOIN keeps every left row, filling the right side with NULL when nothing matches. Any
WHERE condition on a right-side column then runs on those NULLs and removes the row. So the
filter has to happen before the outer join: inside the subquery, or in the ON clause.
Pre-aggregating also keeps the grain clean. The subquery has one row per product, so the outer join
can’t fan out, and COALESCE turns “no delivered sales” into 0.
Under the hood
The subquery is a hash join (order_items probing a hash table of delivered order_ids) feeding a
hash aggregate keyed by product_id. That result, at most 40 rows, becomes the build side of the
outer join. DuckDB marks which build rows were matched; for a left join it emits every probe row
from products either way, with NULLs where the probe missed.
Per-problem discussion threads are coming. Until then, questions and better solutions are welcome on LinkedIn or by email at hello@builtfrombits.com.
Press ⌘ ↵ or Run to check the visible tests. Submit runs the hidden ones too. Everything runs in your browser.