Phase 8 · SQL interview problems, from easy to hard · Topic 8.5

Customers who have never had an order delivered

Must knowInterview problemEasy

List every customer who has never had an order delivered — including customers who have never ordered at all.

Not started

Step 1 Solve it yourself first

Tables you will use

customers orders

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"]
For each customer, ask one question: does a delivered order exist? Keep the customers where the answer is no.

The approach, step by step

  1. Start from customers, because every customer must be considered — including ones with no orders.
  2. Look for a delivered order belonging to each customer.
  3. Keep the customers for whom none exists: , or a LEFT JOIN with the status test inside and WHERE o.order_id IS NULL.
  4. Check: Omar and the second Tom never ordered; Zoe only has a pending order.

Your turn: write it before you look

Ctrl/⌘ + Enter runs it. Select part of the query to run only that part.
Show the solution

Stuck for more than ten minutes? Open it, read it, close it, and write it again from memory.

Solution: NOT EXISTS

sql
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_idname
4Omar
5Zoe
6Tom

Another way: LEFT JOIN with the condition in ON

sql
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_idname
4Omar
5Zoe
6Tom

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.

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.

  1. 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 NULL keeps 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 NULL drops them), and a customer with no orders has a NULL status, which fails o.status = 'delivered'. Every row fails one test or the other, so nothing comes back.

  2. 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.

    sql
    SELECT 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
  3. 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.

    sql
    SELECT 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_idname
  4. 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

Ctrl/⌘ + Enter runs it. Select part of the query to run only that part.

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.