Phase 4 · Subqueries, CTEs and set operations · Topic 4.2

Correlated subqueries, EXISTS, and the NOT IN trap

Must know

A correlated subquery refers to a column of the outer row, so it is worked out once per outer row; EXISTS asks "does the subquery return any row at all?", which makes it the safe way to test for a match.

Not started

Step 1 Understand it

Why you need it. "Each customer's latest order", "employees paid above their own department's average", "customers with at least one delivered order" — the changes for every . And the obvious way to find "no match", , has a trap that returns nothing at all when are around.

Watch it happen

AnimationCorrelated subqueries, and why NOT IN breaks on a NULL
Read the animation as text
  1. A correlated subquery uses a column of the outer row, so it is worked out again for every outer row.
  2. Outer row: order 101, customer 1. The inner query finds customer 1's latest date: 2024-03-10. 101's date is earlier, so it fails.
  3. Order 109, also customer 1: its date equals 2024-03-10, the latest. Kept.
  4. Repeat for every order: each customer's latest order survives — 109, 108, 107, 110.

Picture it

flowchart TD
  A["outer row: customer Priya"] --> B["inner query uses Priya's id:<br/>any delivered order?"]
  B -->|"found one"| K["EXISTS is true: keep Priya"]
  C["outer row: customer Omar"] --> D["inner query uses Omar's id:<br/>any delivered order?"]
  D -->|"none"| X["EXISTS is false: drop Omar"]
A is asked again for each outer row, using that row's values. only needs to know whether at least one row comes back.

The idea

In WHERE o.order_date = (SELECT MAX(o2.order_date) FROM orders o2 WHERE o2.customer_id = o.customer_id), the inner uses o.customer_id from the outer row, so it is re-run (in effect) for each order. EXISTS () is true if the subquery finds at least one row; what it selects doesn't matter, so people write SELECT 1. NOT EXISTS is true when it finds none. The NOT IN trap: x NOT IN (1, 2, NULL) means x <> 1 AND x <> 2 AND x <> NULL, and the last part is always unknown — so the whole test is never true.

The shape of it

sql
WHERE EXISTS (
  SELECT 1 FROM other o
  WHERE o.key = outer_table.key
)

Words in lower case are the parts you fill in.

How it works, step by step

  1. Outer row: order 101 of customer 1. Inner query: the latest order_date for customer 1 is 2024-03-10. 101's date is 2024-01-06, so it fails.
  2. Order 109 of customer 1 has date 2024-03-10, which equals the latest, so it is kept.
  3. This repeats for every order, so each customer's latest order is kept.
  4. EXISTS: for customer Omar, the inner query finds no delivered orders, so EXISTS is false and Omar is dropped.
  5. NOT IN with a list containing NULL is never true, so dept_id NOT IN (SELECT dept_id FROM employees) returns no rows — because Gus's dept_id is NULL.

Analogy

A correlated subquery is a teacher checking each pupil's mark against that pupil's own previous best, not one class-wide number. EXISTS is calling out "is anyone here from Leeds?" — one hand up is enough, and nobody has to say their name.

Where the analogy breaks

A teacher really does check each pupil one at a time. A usually doesn't: the rewrites most correlated subqueries into a that does all the rows at once. Think of it as row by row; don't assume it is slow.

Worked examples

Each customer's latest order (correlated)

sql
SELECT o.customer_id, o.order_id, o.order_date
FROM orders o
WHERE o.order_date = (
  SELECT MAX(o2.order_date)
  FROM orders o2
  WHERE o2.customer_id = o.customer_id
)
ORDER BY o.customer_id;

4 rows

customer_idorder_idorder_date
11092024-03-10
21082024-03-01
31072024-02-20
51102024-03-16

o2.customer_id = o.customer_id is the link to the outer row. If a customer had two orders on their latest day, both would appear.

EXISTS: customers with a delivered order

sql
SELECT c.name
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.customer_id AND o.status = 'delivered'
)
ORDER BY c.customer_id;

3 rows

name
Priya
Tom
Lena

The NOT IN trap

sql
SELECT dept_name
FROM departments
WHERE dept_id NOT IN (SELECT dept_id FROM employees);

No rows

dept_name

HR has no employees, yet nothing comes back. The list contains Gus's NULL, so "not in the list" is never true.

NOT EXISTS gets it right

sql
SELECT d.dept_name
FROM departments d
WHERE NOT EXISTS (
  SELECT 1
  FROM employees e
  WHERE e.dept_id = d.dept_id
);

1 row

dept_name
HR

Must-know points

  • A correlated subquery uses outer-row ; logically it runs once per outer row.
  • EXISTS is true when the subquery returns at least one row; SELECT 1 inside is a convention.
  • NOT EXISTS is the NULL-safe "no match" test. Prefer it to NOT IN.
  • x NOT IN (… NULL …) is never true. x IN (… NULL …) still works for the values that match.
  • Correlated subqueries are clear for "compare with my own group"; (Phase 5) often do the same faster.

Common mistakes

  • Using NOT IN against a column that can be NULL.
  • Forgetting the correlation (WHERE o2.customer_id = o.customer_id), which compares every row with the overall maximum instead.
  • Thinking EXISTS cares what the subquery selects. It only checks whether a row exists.

In other databases

  • All major databases support EXISTS, NOT EXISTS and correlated subqueries the same way. If you must use NOT IN, add WHERE dept_id IS NOT NULL inside the subquery.

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, with the HR example, why NOT IN returned nothing.

    Show answer

    The subquery returns every employee's dept_id, and Gus's is NULL. So for HR (dept 4) the test is 4 NOT IN (1, 1, 1, 2, 2, 3, NULL, 1).

    NOT IN means "not equal to each one": 4 <> 1 4 <> 2 AND … AND 4 <> NULL. The last part is unknown, because nobody can say whether 4 equals an unknown value. TRUE AND unknown is unknown, and only keeps true. So HR is dropped, and so is every other department. One NULL in the list empties the whole result.

    NOT EXISTS asks a different question: "is there any employee row with this dept_id?". For HR the answer is plainly no, so it works.

  2. Writefrom a blank page, then run it

    Using EXISTS, write a query for products that appear in at least one order of Priya's.

    Show answer

    For each product, EXISTS asks: "is there at least one item line for this product, in an order placed by Priya?". It stops at the first one it finds. Only the phone case fails.

    sql
    SELECT p.product_name
    FROM products p
    WHERE EXISTS (
      SELECT 1
      FROM order_items oi
      JOIN orders o    ON o.order_id = oi.order_id
      JOIN customers c ON c.customer_id = o.customer_id
      WHERE oi.product_id = p.product_id AND c.name = 'Priya'
    )
    ORDER BY p.product_name;

    6 rows

    product_name
    Backpack
    Desk lamp
    Headphones
    Mug
    Notebook
    Pen
  3. Predictsay it before you run it

    What does SELECT name FROM employees e WHERE NOT EXISTS (SELECT 1 FROM employees r WHERE r.manager_id = e.emp_id) ORDER BY name; return?

    Show answer

    Chen, Eli, Fatima, Gus and Hana: everyone who manages nobody.

    For each employee e, the subquery looks for anyone whose manager_id is e. Asha (1), Ben (2) and Dara (4) appear as someone's manager, so for them a row exists and they are dropped. Nobody reports to the other five.

    sql
    SELECT name
    FROM employees e
    WHERE NOT EXISTS (
      SELECT 1 FROM employees r WHERE r.manager_id = e.emp_id
    )
    ORDER BY name;

    5 rows

    name
    Chen
    Eli
    Fatima
    Gus
    Hana
  4. Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.

    Make the NOT IN version of the HR query return HR, by changing only the subquery.

    Show answer

    Take the NULL out of the list. With Gus's NULL gone, the list is just 1, 2 and 3, and 4 NOT IN (1, 2, 3) is true.

    Use this whenever the column in a NOT IN subquery can be NULL. Or just use NOT EXISTS.

    sql
    SELECT dept_name
    FROM departments
    WHERE dept_id NOT IN (
      SELECT dept_id FROM employees WHERE dept_id IS NOT NULL
    );

    1 row

    dept_name
    HR

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.

A correlated subquery references the outer query's row, so logically it is evaluated per row — useful for comparisons against a row's own group, like each customer's latest order. EXISTS tests whether a subquery returns any row, and NOT EXISTS is my default , because NOT IN returns nothing if the subquery yields a single NULL: x not in a list containing NULL can never be true.

Follow-up questions

Why is NOT IN dangerous?

x NOT IN (a, NULL) means x <> a AND x <> NULL. The second part is unknown, so the whole condition is never true and no rows return.

EXISTS or — which is better?

For "has a match" they are equivalent and usually optimised the same. For "has no match", use NOT EXISTS because of NULLs.

Are correlated subqueries slow?

Not necessarily: optimisers often turn them into joins. On big data, a window function or a pre- join is usually faster and clearer.

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.