Skip to content

Intermediate: combining and changing data · Subqueries, CTEs and set operations · topic 27 of 62 · 35 min

IN, EXISTS and NOT EXISTS

Explanation

A subquery can return a list, and IN checks a value against that list. EXISTS asks "is there at least one matching row?" and NOT EXISTS asks "are there none?". Beware NOT IN: if the list contains a NULL, NOT IN returns no rows at all, so prefer NOT EXISTS.

Subqueries that return a list

In module 2 you wrote city IN ('Pune', 'Mumbai') with a typed list. The list can also come from a subquery that returns one column and any number of rows. "Customers who have placed an order" means: customers whose id is in the list of customer_ids in orders.

sql
SELECT id, name
FROM customers
WHERE id IN (SELECT customer_id FROM orders);
-- 1 Asha Verma, 2 Ravi Kumar, 3 Priya Nair, 4 Imran Khan, 5 Sneha Patil
Arjun (id 6) is not in the list, so he is left out. Asha appears once even though she has three orders: IN only asks "is it in the list?".

That last point is a real difference from a join. orders JOIN customers would repeat Asha three times. IN (and EXISTS) never multiply rows; each outer row is either kept once or dropped.

Subqueries can be nested. Products that were part of a Delivered order: products whose id is in the order lines whose order_id is in the Delivered orders.

sql
SELECT name FROM products
WHERE id IN (SELECT product_id FROM order_items
             WHERE order_id IN (SELECT id FROM orders WHERE status = 'Delivered'));
-- Basmati Rice 5kg, Toor Dal 1kg, Steel Tiffin Box, Cotton Kurta, Running Shoes, Bluetooth Earbuds

EXISTS: is there at least one?

EXISTS (subquery) is true when the subquery returns at least one row, and false when it returns none. The values in the row do not matter, which is why people write SELECT 1 inside. The subquery usually refers to the outer row (here c.id), which makes it a correlated subquery; topic 3 explains those in detail.

sql
SELECT c.id, c.name
FROM customers AS c
WHERE EXISTS (SELECT 1 FROM orders AS o WHERE o.customer_id = c.id);
-- the same 5 customers
Read it as: keep customer c if there exists an order whose customer_id is c.id. The database can stop looking at the first order it finds.

NOT EXISTS: are there none?

sql
SELECT d.id, d.name
FROM departments AS d
WHERE NOT EXISTS (SELECT 1 FROM employees AS e WHERE e.dept_id = d.id);
-- 5 | Legal
This is the anti-join of module 5 written as a subquery. Most engines run both forms equally fast.

The NOT IN + NULL trap

Try the same question with NOT IN: departments whose id is not in the list of employees' dept_ids.

sql
SELECT name FROM departments
WHERE id NOT IN (SELECT dept_id FROM employees);
-- (no rows!)
Legal should be returned, but nothing comes back.

Why? Deepak's dept_id is NULL, so the list is (1, 1, 1, 1, 2, 2, 2, 3, 3, NULL, 4). id NOT IN (list) means id <> 1 AND id <> 2 AND ... AND id <> NULL. For Legal, 5 <> NULL is unknown, and true AND unknown is unknown, not true. So no department ever passes. One NULL in the list empties the whole result.

FormNULL in the subqueryAdvice
INHarmless for finding matchesFine to use
NOT INReturns no rows at allAvoid, or add WHERE col IS NOT NULL inside
NOT EXISTSHandled correctlyThe safe default for "has none"
LEFT JOIN ... IS NULLHandled correctlyAlso safe (module 5)
sql
SELECT name FROM departments
WHERE id NOT IN (SELECT dept_id FROM employees WHERE dept_id IS NOT NULL);
-- Legal
The fix if you must use NOT IN: remove NULLs from the list.

Common mistakes

  • NOT IN with a column that can hold NULL. Use NOT EXISTS, or filter out the NULLs inside the subquery.
  • Returning two columns from an IN subquery: id IN (SELECT customer_id, status ...) is an error. Return one column.
  • Forgetting to link an EXISTS subquery to the outer row. WHERE EXISTS (SELECT 1 FROM orders) is true for every customer as long as any order exists.
  • Using a JOIN when you only need "has at least one", then fixing the duplicates with DISTINCT. EXISTS expresses the question directly.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

IN, EXISTS and NOT EXISTS · 1/7

IN, EXISTS and NOT EXISTS

A subquery can return a whole list. IN checks whether a value is in it.

Prefer a video by a teacher? Find free videos on “sql exists not exists in subquery not in null trap” on YouTube ↗

In real life: Customers to remind about their cart

Shopping apps send reminders to people who added items to a cart but have not ordered in the last week. That is a NOT EXISTS: customers for whom there exists a cart item, and there does not exist a recent order. Teams prefer NOT EXISTS here because a single NULL customer id in the orders table would make a NOT IN version silently send no reminders at all.

sql
SELECT c.name
FROM customers AS c
WHERE NOT EXISTS (
  SELECT 1 FROM orders AS o
  WHERE o.customer_id = c.id AND o.order_date >= '2025-03-01'
);
Customers with no order since 1 March 2025: Priya Nair and Arjun Reddy.

Practice (0/4 done)

Task 1 · Code · SQL

Which customers have placed at least one order? Use IN or EXISTS (not a join). Return id and name.

Task 2 · Code · SQL

Which departments have no employees? Use NOT EXISTS. Return the department id and name.

Task 3 · Code · SQL

The starter query should list departments with no employees, but it returns nothing, because one employee has a NULL dept_id. Keep NOT IN, but fix the query so it returns the right department. Return one column: name.

Task 4 · Code · SQL

Which products were part of at least one Delivered order? Use nested IN subqueries (products → order_items → orders). Return one column: name.

Quick quiz

Question 1

1.The subquery SELECT dept_id FROM employees returns 1, 2, 3, 4 and one NULL. How many rows does WHERE id NOT IN (that subquery) return from departments (ids 1 to 5)?

Question 2

2.What does EXISTS (subquery) test?

Question 3

3.Asha has three orders. How many times does she appear in SELECT name FROM customers WHERE id IN (SELECT customer_id FROM orders)?

Question 4

4.Which is the safest way to find customers who have never ordered, if orders.customer_id might contain NULLs?

Question 5

5.WHERE EXISTS (SELECT 1 FROM orders) without any link to the outer row is true...

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

The subquery SELECT dept_id FROM employees returns 1, 2, 3, 4 and one NULL. How many rows does WHERE id NOT IN (that subquery) return from departments (ids 1 to 5)?

Answer: 0

id <> NULL is unknown, so the NOT IN is never true for any row.

What does EXISTS (subquery) test?

Answer: Whether the subquery returns at least one row

The values do not matter, only whether any row comes back.

Asha has three orders. How many times does she appear in SELECT name FROM customers WHERE id IN (SELECT customer_id FROM orders)?

Answer: Once

IN keeps or drops each outer row once; it never multiplies rows like a join.

Which is the safest way to find customers who have never ordered, if orders.customer_id might contain NULLs?

Answer: WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id)

NOT EXISTS is not affected by NULLs in the subquery.

WHERE EXISTS (SELECT 1 FROM orders) without any link to the outer row is true...

Answer: for every outer row, as long as the orders table has any row

Without o.customer_id = c.id, the subquery does not depend on the customer.

A correlated subquery is slow on a table of 10 lakh rows. What is a good first step?Related

Answer: Check the plan, index the linking column, or rewrite it as a join to a grouped derived table

In principle it runs once per outer row; an index or a rewrite helps.

From Correlated subqueries and derived tables

A scalar subquery returns no rows. What value does it give?Related

Answer: NULL

An empty scalar subquery gives NULL.

From Scalar subqueries

Why is NOT EXISTS often safer than NOT IN?Related

Answer: NOT IN with a NULL in the list returns no rows at all

x NOT IN (..., NULL) is never true, so the query silently returns nothing. NOT EXISTS has no such trap.

From Subqueries, CTEs and window functions

In SQLite, how do you see how many rows the last UPDATE changed?Related

Answer: SELECT changes()

changes() returns the rows changed by the most recent INSERT, UPDATE or DELETE.

From UPDATE and DELETE safely

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 4 more topics in “Subqueries, CTEs and set operations” before its exam: Correlated subqueries and derived tables, Common table expressions (WITH), Recursive CTEs, UNION, INTERSECT and EXCEPT.
  2. Module 6 exam: subqueries, CTEs and set operations · 14 questions · 25 min, without help. 6 topics of this module still to finish first.
  3. Then the next module, Window functions. 62 topics left in the course before the final exam.

← Previous: Scalar subqueries