Step 1 Solve it yourself first
Tables you will use
What it really tests
Anti-, and the classic trap of putting a condition on the right-hand in after a , which silently removes the customers you are looking for.
The pattern: with no match ()
"Never", "no", "without", "haven't" — you are looking for something that isn't there.
Picture it
flowchart LR
C["every customer"] --> Q{"any delivered order<br/>for this customer?"}
Q -->|"yes: Priya, Tom, Lena"| X["dropped"]
Q -->|"no: Omar, Zoe, Tom 6"| K["kept"]The approach, step by step
- Start from customers, because every customer must be considered — including ones with no orders.
- Look for a delivered order belonging to each customer.
- Keep the customers for whom none exists: , or a LEFT JOIN with the status test inside and
WHERE o.order_id IS NULL. - Check: Omar and the second Tom never ordered; Zoe only has a pending order.
Your turn: write it before you look
Your queries in this box
Show the solution
Stuck for more than ten minutes? Open it, read it, close it, and write it again from memory.
Solution: NOT EXISTS
SELECT c.customer_id, c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.status = 'delivered'
)
ORDER BY c.customer_id;3 rows
| customer_id | name |
|---|---|
| 4 | Omar |
| 5 | Zoe |
| 6 | Tom |
Another way: LEFT JOIN with the condition in ON
SELECT c.customer_id, c.name
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
AND o.status = 'delivered'
WHERE o.order_id IS NULL
ORDER BY c.customer_id;3 rows
| customer_id | name |
|---|---|
| 4 | Omar |
| 5 | Zoe |
| 6 | Tom |
The status test must be in ON. In WHERE, o.status = 'delivered' AND o.order_id IS NULL could never both be true, and the would return nothing.
Edge cases to say out loud
- Customers with no orders at all: must be included — that is why we start from customers.
- Customers whose only orders are cancelled or pending (Zoe): included.
- Orders with a status: they are not delivered, so they don't count — NOT EXISTS handles this correctly.
Common mistakes
LEFT JOIN … WHERE o.status <> 'delivered': drops customers with no orders and keeps customers who also have delivered orders.WHERE customer_id NOT IN (SELECT customer_id FROM orders WHERE status = 'delivered')is fine here only because customer_id is in orders — say that out loud.- Starting from orders, which can never return customers who have no orders.
Step 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and more practice.
-
Practice
LeetCode — Top SQL 50 study plan
free account
-
Practice
DataLemur — anti-join questions
real interview questions, many free
Step 3 Practise
Try each one yourself first: out loud, or written and run in the practice box below, which saves every query you run. Then open the answer and compare.
-
ExplainSay what it does and when to use it, in plain words, with no notes.
Explain why the status condition must go in ON, not WHERE, in the LEFT JOIN version.
Show answer
In the LEFT JOIN version, the ON decides what counts as a match: "an order of this customer that was delivered". A customer with no delivered order finds no match, so they get one row with NULLs, and
WHERE o.order_id IS NULLkeeps exactly those customers.Move the status test into the WHERE and it runs after the join, on every row. A customer whose orders are all pending gets matched to those orders (not NULL, so
IS NULLdrops them), and a customer with no orders has a NULL status, which failso.status = 'delivered'. Every row fails one test or the other, so nothing comes back. -
Writefrom a blank page, then run it
List products that have never been part of a delivered order.
Show answer
For each product, look for any item line in a delivered order. Only the phone case has none: it has never been sold at all. Every other product appears in at least one delivered order.
sqlSELECT p.product_name FROM products p WHERE NOT EXISTS ( SELECT 1 FROM order_items oi JOIN orders o ON o.order_id = oi.order_id WHERE oi.product_id = p.product_id AND o.status = 'delivered' ) ORDER BY p.product_name;1 row
product_name Phone case -
Predictsay it before you run it
What does the LEFT JOIN version return if you move the status test into WHERE?
Show answer
No rows at all. After the LEFT JOIN, every row either has an order (so order_id is not NULL) or has no order (so status is NULL, and
NULL = 'delivered'isn't true). A row would need a delivered status and a NULL order_id at once, which is impossible. A filter on the right-hand table in WHERE quietly cancels the LEFT JOIN.sqlSELECT c.customer_id, c.name FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id WHERE o.status = 'delivered' AND o.order_id IS NULL ORDER BY c.customer_id;No rows
customer_id name -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
What should happen to a customer whose only order has a NULL status? Does your query do that?
Show answer
They have never had an order delivered, so they should be listed. (In this status is NOT NULL, so it can't happen, but it is worth checking.)
The NOT EXISTS solution gets it right: inside the ,
NULL = 'delivered'is not true, so no delivered order is found, NOT EXISTS is true, and the customer is listed. The LEFT JOIN version also lists them, because the ON finds no match.The version that gets it wrong flips the logic: "customers who have an order WHERE status <> 'delivered'".
NULL <> 'delivered'is not true either, so that customer would be missed. Whenever you write a negative test, ask what it does with NULL.
Practice box: write and run your answers here
Your queries in this box
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud.
This is an anti-join. I drive it from customers so customers with no orders are included, and keep those for which NOT EXISTS finds a delivered order. Equivalently, a LEFT JOIN with the status filter inside the ON clause and WHERE the order id . The trap is putting the status filter in WHERE, which turns the left join into an .
Follow-up questions
Why not ?
If the subquery could return a NULL, NOT IN returns nothing. NOT EXISTS is always safe.
How would you also show how many orders each of these customers has?
LEFT JOIN orders (all statuses) and COUNT(o.order_id), grouped by customer, keeping the anti-join as a filter.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you have read it. Be strict: if you had to look at the notes, it is not ticked yet.
- Explain Say what it does and when to use it, in plain words, with no notes.
- Write Write the query from a blank page, then run it to check.
- Predict Read a query and say which rows it returns before you run it.
- Edge cases Say what happens with NULLs, ties, duplicates or no rows at all, and fix it.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.