Hook: Interviewers love this one because it looks simple, but it reveals whether you understand how SQL finds the “missing” rows, not just the matching ones.
Question: How do you return customers who have never placed an order?
Answer: Use an ANTI-JOIN pattern, usually with LEFT JOIN plus a NULL check, or with NOT EXISTS. The idea is to keep every customer, try to match orders, and then filter to the customers where no match was found. The safest beginner-friendly version is often NOT EXISTS because it reads like plain English: “give me customers for which no related order exists.”
Interview-Ready Answer: “I’d query customers and exclude anyone who has at least one matching row in orders. In practice, I usually write NOT EXISTS because it clearly expresses the anti-join and avoids duplicates from multiple orders. A common alternative is LEFT JOIN with WHERE o.order_id IS NULL, which also works when the joined key is guaranteed non-null.”
Detailed Explanation: This is a classic “find the missing relationship” problem. In database terms, you want an anti-join, which means “rows from the left table that do not have a match in the right table.” The important mental shift is that you are not selecting orders; you are proving absence of orders.
customers table. Every customer is a candidate.orders using customer_id.customer_id, name, or email.With LEFT JOIN, SQL keeps all rows from the left side and fills unmatched right-side columns with NULL. Then WHERE o.order_id IS NULL filters to only those unmatched rows. With NOT EXISTS, the database checks whether any matching order row exists for each customer and returns false when it finds one.
| Pattern | Best for | Gotcha |
|---|---|---|
| LEFT JOIN + IS NULL | Readable, common interviews | Do not filter on a nullable business column |
| NOT EXISTS | Clear anti-join logic | Can look unfamiliar to beginners |
| NOT IN | Small toy examples only | Breaks if subquery returns NULL |
Why NOT IN is risky: if the subquery contains even one NULL, the comparison can become unknown for every row, and you may get no results at all. That is a favorite interview trap.
For most real databases, NOT EXISTS and LEFT JOIN ... IS NULL often compile into a similar anti-join plan. With an index on orders(customer_id), the database can usually find matches quickly. A realistic setup might be a customer table with 1 million rows and orders with 10 million rows; the index lets the engine avoid scanning all 10 million orders for each customer. Without an index, the query can degrade toward a nested-loop style check and become slow.
Think in rough terms: with a good index, this is often close to O(C log O) or better in practice; without one, it can behave much worse, especially on large tables. Interviewers like hearing that you care about the index on the foreign key column.
NOT EXISTS, but LEFT JOIN can duplicate customer rows before filtering, so make sure you filter on the right-side null correctly.orders.customer_id can be NULL, those rows do not match any customer. That does not change the anti-join result, but it means bad data exists.order_id, for the null check.Memory hook: imagine every customer carrying a “receipt card.” Orders stamp the card. The customers with no orders are the ones whose card is still blank.
Real-World Story: In an e-commerce checkout service, the product team wants a list of customers who signed up but never bought anything. Marketing uses that list for a reminder email campaign, and analytics uses it to measure conversion from signup to first purchase.
What goes wrong when this is misunderstood? A developer writes a query that joins customers to orders and filters on the wrong column, or uses NOT IN against a subquery that contains NULL. The result is either too few customers or zero customers. In production, the symptom might be a dashboard that suddenly says “0 non-buyers,” a campaign that sends no emails, or logs showing the query returned empty even though support can clearly see new users without orders. Users are affected because the business targets the wrong audience, and the team loses trust in the report.
-- Demonstrates two correct ways to find customers with no orders.
-- This script is written in PostgreSQL-friendly SQL.
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS customers;
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
customer_name TEXT NOT NULL
);
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT NULL,
order_total NUMERIC(10,2) NOT NULL,
CONSTRAINT fk_orders_customers
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
INSERT INTO customers (customer_id, customer_name) VALUES
(1, 'Ava'),
(2, 'Ben'),
(3, 'Cora'),
(4, 'Dan');
INSERT INTO orders (order_id, customer_id, order_total) VALUES
(101, 1, 25.00),
(102, 1, 40.00),
(103, 3, 18.50),
(104, NULL, 99.99); -- Bad/messy data: should not match any customer
-- Option 1: LEFT JOIN + IS NULL
-- Why it works: customers with no matching order get NULLs on the order side.
SELECT
c.customer_id,
c.customer_name
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL
ORDER BY c.customer_id;
-- Option 2: NOT EXISTS
-- Why it works: return customers for whom no order row exists.
SELECT
c.customer_id,
c.customer_name
FROM customers AS c
WHERE NOT EXISTS (
SELECT 1
FROM orders AS o
WHERE o.customer_id = c.customer_id
)
ORDER BY c.customer_id;
-- Edge-case demo: this is the WRONG pattern when NULLs can appear in the subquery.
-- Because orders.customer_id contains NULL, NOT IN may return no rows at all.
SELECT
c.customer_id,
c.customer_name
FROM customers AS c
WHERE c.customer_id NOT IN (
SELECT o.customer_id
FROM orders AS o
)
ORDER BY c.customer_id;
Follow-up & Tricky Questions:
NOT EXISTS instead of LEFT JOIN? Yes. It is often the cleanest expression of the anti-join because it directly states that no related row should exist.NOT EXISTS usually safer than NOT IN? Because NOT IN can fail logically when the subquery returns NULL, while NOT EXISTS handles missing values more naturally.orders(customer_id). That helps the database find matching orders quickly for each customer.NOT EXISTS handles this cleanly, and LEFT JOIN needs the null filter after the join.LEFT JOIN with GROUP BY and HAVING COUNT(o.order_id) = 0, or aggregate after the anti-join.WHERE o.customer_id IS NULL? Only if you are certain the joined column cannot be null for a real order row. Safer is to check the non-null primary key, like o.order_id IS NULL.SELECT * matter here? It can hide duplicate columns and make review harder. In interviews, naming just the needed customer fields shows you understand the shape of the result.LEFT JOIN problem or a NOT EXISTS problem? It is really an anti-join problem. Both are valid; the best choice depends on readability and null safety.Common Mistakes:
INNER JOIN. Correction: An inner join only keeps matches, so it removes the very customers you want to find.NOT IN on a subquery that can return NULL. Correction: Prefer NOT EXISTS or filter nulls very carefully.NULL after a left join. Correction: Check a guaranteed non-null right-side column, usually the primary key.Memory Hook: “Keep all customers, stamp orders, then show the blank cards.” If the card is blank, that customer has no orders.
Cheat Sheet:
NOT EXISTS is the clearest anti-join.LEFT JOIN ... IS NULL is the classic alternative.NOT IN if nulls are possible.orders(customer_id) for speed.order_id.Practice Tasks:
LEFT JOIN, then rewrite it with NOT EXISTS.NULL customer_id into orders and observe why NOT IN becomes dangerous.