Multi Row Subqueries: IN, ANY and ALL
Subqueries that return a list of values, and the three operators that consume them - including the NULL trap that makes NOT IN return nothing.
- SQL Basics
- DDL
- DML
- SELECT
- WHERE
- Functions
- NULL and Logic
- Aggregate Functions
- GROUP BY
- JOIN
- Subqueries
- Set Operations
- CTEs
- Constraints
- Keys
- Relationships
- Database Design
- Normalisation
- Views
- Window Functions
- Advanced SQL
- Procedures and Functions
- Triggers
- Temporary Tables
- Transactions
- Isolation and Locking
- Indexes
- Query Performance
- SQL Security
- SQL Dialects
- Practical SQL
Concept
When a subquery returns many rows, the outer query needs an operator that accepts a set: IN, ANY (also spelled SOME) or ALL.
| Operator | True when | Equivalent |
|---|---|---|
x IN (set) | x equals at least one member | x = ANY (set) |
x > ANY (set) | x is greater than the smallest member | x > (SELECT MIN(...)) |
x > ALL (set) | x is greater than the largest member | x > (SELECT MAX(...)) |
x NOT IN (set) | x differs from every member | x <> ALL (set) |
Example
-- IN: employees in any department located in Pune or Mumbai
SELECT first_name, dept_id
FROM employees
WHERE dept_id IN (SELECT id FROM departments WHERE location IN ('Pune', 'Mumbai'));
-- ANY: paid more than at least one person in department 10
SELECT first_name, salary
FROM employees
WHERE salary > ANY (SELECT salary FROM employees WHERE dept_id = 10);
-- ALL: paid more than everyone in department 10
SELECT first_name, salary
FROM employees
WHERE salary > ALL (SELECT salary FROM employees WHERE dept_id = 10);Explanation
ANY and ALL are worth reading twice. > ANY means "greater than the minimum", because being greater than at least one member is easiest to satisfy against the smallest. > ALL means "greater than the maximum". Most people find > (SELECT MAX(...)) clearer, and it is exactly equivalent - with one exception noted below.
The NOT IN trap
-- Returns NOTHING, because one employee has dept_id = NULL
SELECT name FROM departments
WHERE id NOT IN (SELECT dept_id FROM employees);
-- Fix 1: exclude the NULLs
SELECT name FROM departments
WHERE id NOT IN (SELECT dept_id FROM employees WHERE dept_id IS NOT NULL);
-- Fix 2, preferred: NOT EXISTS is NULL safe by construction
SELECT d.name FROM departments d
WHERE NOT EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.id);x NOT IN (10, 20, NULL) expands to x <> 10 AND x <> 20 AND x <> NULL. The last term is always UNKNOWN, so the whole expression can never be TRUE. This affects <> ALL identically, since it is the same thing.
Empty sets behave differently
-- Against an empty set:
-- x IN (empty) is FALSE - nothing matches
-- x > ALL (empty) is TRUE - vacuously, there is nothing to exceed
-- x > ANY (empty) is FALSE - there is nothing to beat
SELECT first_name FROM employees
WHERE salary > ALL (SELECT salary FROM employees WHERE dept_id = 999);That query returns every employee, which surprises people. > (SELECT MAX(salary) ... WHERE dept_id = 999) returns no rows instead, because MAX of nothing is NULL. This is the one case where ALL and MAX genuinely differ.
Important rules
INwithNULLs in the list is safe.NOT INwith even oneNULLreturns no rows.= ANYisIN;<> ALLisNOT IN.- Against an empty set,
ALLis true andANYis false. - The subquery must return exactly one column for
IN, unless the dialect supports row constructors:WHERE (a, b) IN (SELECT x, y FROM ...)works in MySQL and PostgreSQL.
Common mistakes
NOT INagainst a nullable column - the classic silent empty result.- Reading
> ANYas "greater than all of them". - Assuming an empty subquery makes
> ALLreturn nothing. - Using
INwith a huge subquery result when a join orEXISTSwould be far cheaper.
Best practices
- Default to
EXISTSandNOT EXISTSfor existence questions; useINfor genuinely small, known lists. - Write
> (SELECT MAX(...))instead of> ALLwhen readability matters - and be aware of the empty set difference. - Always add
WHERE col IS NOT NULLinside aNOT INsubquery if you keep the form. - Make the subquery column
NOT NULLat the schema level where the domain allows it.
Practice
- Find customers who have placed at least one shipped order, using
IN. - Find the product that is more expensive than every product in the
servicecategory, using bothALLandMAX. - Demonstrate the
NOT INNULL trap on the sample data and fix it twice.