Step 1 Understand it
Why you need it. "Customers who never ordered", "products that never sold", "employees with no manager on file" — finding what is absent is one of the most common interview questions and real tasks. You can't find it with alone, because the missing aren't there to filter.
New words in this topic (tap one to see what it means)
Watch it happen
Read the animation as text
- Step 1: LEFT JOIN customers to orders. Every customer is kept; those with no orders get one row full of NULLs on the orders side.
- Step 2: keep only the unmatched rows: WHERE o.order_id IS NULL. order_id is the orders primary key, so it is NULL only when there was no match at all.
- The customers who never ordered: Omar and the second Tom. That is an anti-join.
Picture it
flowchart LR C["all 6 customers"] -->|"LEFT JOIN orders"| J["4 with orders: matched<br/>Omar, Tom 6: NULL orders"] J -->|"WHERE order_id IS NULL"| R["Omar, Tom 6<br/>never ordered"]
The idea
Do a LEFT JOIN, so every left row is kept and unmatched ones get on the right. Then keep only the unmatched ones: WHERE o.order_id IS NULL. Test a that can never be NULL in a real match — the right 's is perfect. says the same thing more directly: "keep the customer if no order exists for them". Its opposite, the , keeps rows that do have a match, each only once: WHERE EXISTS (…) or WHERE id IN (…).
The shape of it
SELECT a.*
FROM table_a a
LEFT JOIN table_b b ON b.key = a.key
WHERE b.key IS NULL;Words in lower case are the parts you fill in.
How it works, step by step
- customers LEFT JOIN orders keeps all 6 customers; Omar and the second Tom get NULLs for every order column.
WHERE o.order_id IS NULLkeeps only those NULL rows: the customers with no orders.- order_id is the orders primary key, so it is NULL only when there was no match — never in a real order.
- NOT EXISTS version: for each customer, look for any order with their id; keep the customer if there is none.
- Avoid for this when the inner list can hold a NULL — it returns nothing (Phase 4 shows why).
Analogy
Checking a guest list against the sign-in sheet. Hold the list (left table) next to the sheet (right table); the names with an empty space next to them on the sheet are the people who didn't come.
Where the analogy breaks
With a list you just look for blanks. In you must test the right column for the blank: test a column that might legitimately be NULL (like amount) and you will wrongly report customers whose real order just has no amount.
Worked examples
Customers who never ordered: LEFT JOIN …
SELECT c.customer_id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL
ORDER BY c.customer_id;2 rows
| customer_id | name |
|---|---|
| 4 | Omar |
| 6 | Tom |
Products never sold: NOT EXISTS
SELECT p.product_name
FROM products p
WHERE NOT EXISTS (
SELECT 1
FROM order_items oi
WHERE oi.product_id = p.product_id
)
ORDER BY p.product_name;1 row
| product_name |
|---|
| Phone case |
For each product, the inner looks for any order line with that product. NOT EXISTS keeps the product when it finds none. Phase 4 explains EXISTS fully.
The opposite, a semi-join: rows that do have a match
SELECT dept_name
FROM departments
WHERE dept_id IN (SELECT dept_id FROM employees)
ORDER BY dept_name;3 rows
| dept_name |
|---|
| Engineering |
| Marketing |
| Sales |
Each department appears once, however many employees it has. A JOIN would repeat Engineering four times.
Must-know points
- = rows with no match:
LEFT JOIN … WHERE right.pk IS NULL, orNOT EXISTS. - Semi-join = rows with at least one match, each once:
EXISTSorIN. - In the IS NULL test, use a column that is never NULL in a real match — the right table's primary key or join key.
- NOT EXISTS is the safest anti-join: it handles NULLs correctly, and optimisers run it efficiently.
- Don't use
NOT IN (subquery)if the can return NULL.
Common mistakes
- Testing a nullable column (
WHERE o.amount IS NULL), which also returns Lena because order 106 has no amount. - Using and wondering why the missing rows never appear.
- Using
NOT IN (SELECT dept_id FROM employees): Gus's NULL dept_id makes it return no rows at all.
In other databases
- Every supports both forms. Some (, Oracle) also have / MINUS for "ids in A but not in B".
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.
- Read
-
Practice
PostgreSQL Exercises — Joins and Subqueries
pgexercises.com, free
-
Practice
LeetCode — Top SQL 50 study plan
free account
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 the two ways to write an anti-join and why the IS NULL column choice matters.
Show answer
1. LEFT JOIN, then keep the rows with no match:
LEFT JOIN orders o ON o.customer_id = c.customer_id WHERE o.order_id IS NULL. Unmatched rows get NULLs in every right-hand column, so you keep just those.
2. NOT EXISTS:WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id). Keep the row if the subquery finds nothing.The IS NULL column matters because NULL can mean two things: "no match" or "matched, but this value is empty". Test a column that is never NULL in a real match: the right table's primary key, or the column you joined on. If you test amount instead, Lena's order 106 (which has no amount) looks like "no order", and Lena wrongly appears.
-
Writefrom a blank page, then run it
Write a query for departments with no employees, using LEFT JOIN … IS NULL.
Show answer
Departments on the left. A department with no employees gets NULL in e.emp_id, and emp_id is the primary key, so it is never NULL for a real employee. Only HR is left.
sqlSELECT d.dept_id, d.dept_name FROM departments d LEFT JOIN employees e ON e.dept_id = d.dept_id WHERE e.emp_id IS NULL ORDER BY d.dept_id;1 row
dept_id dept_name 4 HR -
Predictsay it before you run it
What does the first example return if you change
WHERE o.order_id IS NULLtoWHERE o.amount IS NULL?Show answer
Lena, Omar and the second Tom (id 6), instead of just Omar and Tom.
Omar and Tom have no orders, so their order columns are NULL, amount included. Lena does have orders, but one of them, 106, has a NULL amount, so that row passes the test too. A NULL amount can mean "no order" or "an order with no amount", and this test can't tell them apart. That is why you test the key.
sqlSELECT c.customer_id, c.name FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id WHERE o.amount IS NULL ORDER BY c.customer_id;3 rows
customer_id name 3 Lena 4 Omar 6 Tom -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Run
SELECT dept_name FROM departments WHERE dept_id NOT IN (SELECT dept_id FROM employees);. Explain the result, then fix it.Show answer
It returns no rows at all, even though HR has no employees.
The subquery returns every employee's dept_id, and Gus's is NULL. So for HR the test is
4 NOT IN (1, 1, 1, 2, 2, 3, NULL, 1), which means "4 <> 1 … AND 4 <> NULL".4 <> NULLis unknown, and anything AND unknown can't be true, so HR is not kept. One NULL in the list makes NOT IN fail for every row.Fix it with NOT EXISTS, which doesn't have this trap, or keep NOT IN but add
WHERE dept_id IS NOT NULLinside the subquery.sqlSELECT dept_name FROM departments d WHERE NOT EXISTS ( SELECT 1 FROM employees e WHERE e.dept_id = d.dept_id ) ORDER BY dept_name;1 row
dept_name HR
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.
To find rows with no match I use an anti-join: either a LEFT JOIN with WHERE the right table's key IS NULL, or NOT EXISTS with a . I prefer NOT EXISTS because it states the intent directly and is NULL-safe, and I avoid NOT IN against a subquery, since a single NULL in that list makes the whole condition unknown and returns nothing.
Follow-up questions
Why not use NOT IN to find customers without orders?
If the subquery returns any NULL, x NOT IN (…) is never true, so you get no rows. NOT EXISTS has no such problem.
Which column should you test for IS NULL after the LEFT JOIN?
One that is never NULL in a real matched row — the right table's primary key or the join column.
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.