SQL Foundations · 15 min · 150 XP

Subqueries: IN, EXISTS, or a join?

Filter one table by what's in another with IN and EXISTS, and know when a join is the wrong tool.

A subquery is a query in brackets inside another query. The inner query produces a value or a list, and the outer query uses it. That lets you ask about one table using facts that live in another, without joining the two.

Customers who have had an order refunded
SELECT name
FROM customers
WHERE id IN (
  SELECT customer_id FROM orders WHERE status = 'refunded'
);

-- Maja Berg
-- Aisha Khan

A subquery that returns a single value can go anywhere a number can. The average list price is 76.125, so this returns the three products priced above it: Trail Runner Shoes, Rain Shell Jacket and Trekking Poles. When prices change, the query still works, because nothing is hard-coded.

sql
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);

EXISTS asks whether a matching row exists at all. Its subquery can refer to the row the outer query is looking at, here c.id, so it runs once per customer. NOT EXISTS finds customers with no orders: the same Omar Haddad the LEFT JOIN found last lesson.

sql
SELECT c.name
FROM customers AS c
WHERE NOT EXISTS (
  SELECT 1 FROM orders AS o WHERE o.customer_id = c.id
);

-- Omar Haddad

Be careful with NOT IN. x NOT IN (1, 2, NULL) means x <> 1 AND x <> 2 AND x <> NULL, and a comparison with NULL is never true. So if the subquery's list contains a single NULL, NOT IN matches nothing, with no error. NOT EXISTS doesn't have this problem, which is why it's the safer habit.

Subquery or join? Use a join when you need columns from the other table. Use IN or EXISTS when you only need to know whether a match exists. A join returns a row for every match, so a customer who bought the same product on two orders comes back twice. IN and EXISTS never repeat an outer row.

Scenario: two ways to list who bought Wool Socks (product 6)
-- A: returns 4 rows
SELECT c.name
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id
JOIN order_items AS i ON i.order_id = o.id
WHERE i.product_id = 6;

-- B: returns 3 rows
SELECT name
FROM customers
WHERE id IN (
  SELECT o.customer_id
  FROM orders AS o
  JOIN order_items AS i ON i.order_id = o.id
  WHERE i.product_id = 6
);

Loading your workspace…