Brightlane's customer success team wants a complete record of every customer, every order they've placed, and every item in those orders. Customers with no orders must still appear in the output. Orders with no items must still appear too.
Write a query to return the customer name, order ID, and item ID for every row in the result.
Assumptions:
- The chain links three tables:
customers→orders→order_items. - The result is anchored on the customer side. A customer with no orders appears once with
order_idanditem_idboth missing. An order with no items appears withitem_idmissing. - A customer with multiple items contributes multiple rows.
Output:
- One row per matched customer-order-item triple, plus one row per customer with no orders, plus one row per order with no items, with columns
customer_name,order_id, anditem_id.
Schema · ecommerce 5 tables
Run previews · Check grades
Write a query, then run it to see results here.
Worked solution Try it yourself first
SELECT
c.name AS customer_name,
o.id AS order_id,
oi.id AS item_id
FROM
customers c
LEFT JOIN orders o ON c.id = o.customer_id
LEFT JOIN order_items oi ON o.id = oi.order_id The shape
Every link in the chain is a LEFT JOIN because every link must preserve unmatched rows on the left. A customer with no orders stays in the result with both order_id and item_id as NULL; an order with no items stays with item_id as NULL. The moment any link became an INNER JOIN, the rows it dropped would be gone for good — no later LEFT JOIN can recover them.
Clause by clause
SELECT c.name AS customer_name, o.id AS order_id, oi.id AS item_idpicks the identifier from each level of the chain. The twoidcolumns fromordersandorder_itemsare aliased so the result row exposes the chain's structure at a glance.FROM customers canchors the chain on the customer side. The choice of anchor matters:LEFT JOINpreserves rows from the left, so anchoring on customers is what makes "customers with no orders must still appear" possible.LEFT JOIN orders o ON c.id = o.customer_idattaches every order for each customer. Customers with no orders survive, witho.idset toNULLon those rows.LEFT JOIN order_items oi ON o.id = oi.order_idattaches every line item for each surviving order. Orders with no items survive, withoi.idset toNULL. Customer rows that already haveo.id = NULLfrom the previous step pass through this join withoi.idalsoNULL, becauseNULL = NULLis unknown and never matches — the join condition fails, theLEFT JOINkeeps the row anyway, andoi.idlands asNULL.
The trap
The trap is mixing in a single INNER JOIN and assuming the later LEFT JOIN can recover what the inner one dropped. It cannot. Joins evaluate sequentially: each one operates on the intermediate result from the previous step. Once an INNER JOIN discards the no-orders customers, they are not in the intermediate result the next join sees, and no amount of left-side preservation can put them back. The way to keep the chain honest is to read each join in source order and ask, at every step, "does this join discard rows I need later?" If the answer is yes, the join type is wrong.
You practiced a chain of LEFT JOINs where every link must preserve unmatched rows. Once an INNER JOIN drops a row from the intermediate result, no later LEFT JOIN can recover it.