SQL EXISTS and NOT EXISTS: Syntax and Examples
EXISTS is true when its subquery returns at least one row, and false when it returns none. You use it to keep the rows that have a related row somewhere else, such as customers who have placed an order, and NOT EXISTS to keep the rows that have none.
What Is EXISTS in SQL?
EXISTS answers a yes/no question for each row: "is there at least one matching row over there?" It never returns data from the subquery, only true or false. Here it finds the customers who have placed at least one order since 1 July 2024:
SELECT u.id, u.first_name, u.last_name
FROM users u
WHERE EXISTS (SELECT 1
FROM orders o
WHERE o.user_id = u.id
AND o.order_date >= '2024-07-01')
ORDER BY u.id
LIMIT 6;
▶ Run this query in the SQL Playground
| id | first_name | last_name |
|---|---|---|
| 4 | Michael | Carter |
| 10 | Nancy | Wilson |
| 11 | Ronald | Walker |
| 22 | Anthony | Hernandez |
| 30 | Richard | Harris |
| 33 | Mark | Robinson |
For each user, the subquery looks for an order that belongs to that user and is recent enough. If it finds one, the user is kept; if not, the user is dropped. A customer with five recent orders still appears once.
You can also see the true/false value directly. SQLite shows true as 1 and false as 0:
SELECT EXISTS (SELECT 1 FROM orders WHERE total > 4900) AS any_order_over_4900,
EXISTS (SELECT 1 FROM orders WHERE total > 10000) AS any_order_over_10000;
▶ Run this query in the SQL Playground
| any_order_over_4900 | any_order_over_10000 |
|---|---|
| 1 | 0 |
EXISTS Syntax
SELECT columns
FROM outer_table t
WHERE EXISTS (SELECT 1
FROM other_table o
WHERE o.key = t.key); -- the correlation
| Part | Meaning |
|---|---|
EXISTS ( ... ) | True if the subquery returns at least one row |
SELECT 1 | What the subquery selects does not matter; 1 just says "a row" |
o.key = t.key | Links the subquery to the current outer row. This makes it a correlated subquery |
NOT EXISTS ( ... ) | True if the subquery returns no rows |
SELECT 1 is the usual convention because it tells the reader no columns are being used.How EXISTS Is Evaluated
Logically, the database takes the outer rows one at a time, plugs the current row's values into the subquery and runs it. As soon as one matching row turns up, EXISTS is true and the search for that row can stop. This early stop is called short-circuit evaluation, and it is why EXISTS does not care how many matches there are.
| Outer row | Subquery finds | EXISTS | Row kept? |
|---|---|---|---|
| A customer with 3 orders | 3 rows (it can stop after the first) | true | Yes, once |
| A customer with 1 order | 1 row | true | Yes |
| A customer with no orders | Nothing | false | No |
Real databases rarely run the subquery literally once per row. Most optimizers turn EXISTS into a semi-join, which keeps each outer row that has at least one match. The result is the same either way.
NOT EXISTS: Rows With No Match
NOT EXISTS keeps the rows for which the subquery finds nothing. It is the standard way to find "missing" records: customers who never ordered, products that never sold, employees who manage nobody.
SELECT p.id, p.name, p.category
FROM products p
WHERE NOT EXISTS (SELECT 1
FROM order_items oi
WHERE oi.product_id = p.id);
▶ Run this query in the SQL Playground
| id | name | category |
|---|---|---|
| 110 | The Manager's Path | Books |
Only 1 of the 120 products has never appeared on an order line. NOT EXISTS is also safe when the other column contains NULLs, which NOT IN is not.
EXISTS and NOT EXISTS together
Combine the two to describe a group precisely. These are the customers who have ordered but have never had an order cancelled or refunded:
SELECT COUNT(*) AS customers
FROM users u
WHERE EXISTS (SELECT 1 FROM orders o
WHERE o.user_id = u.id)
AND NOT EXISTS (SELECT 1 FROM orders o
WHERE o.user_id = u.id
AND o.status IN ('cancelled', 'refunded'));
▶ Run this query in the SQL Playground
| customers |
|---|
| 74 |
EXISTS Across Several Tables
The subquery can join tables of its own while staying correlated to the outer row. This finds the customers who have at least one order with a successful payment:
SELECT COUNT(*) AS customers_with_successful_payment
FROM users u
WHERE EXISTS (SELECT 1
FROM orders o
JOIN payments p ON p.order_id = o.id
WHERE o.user_id = u.id
AND p.status = 'success');
▶ Run this query in the SQL Playground
| customers_with_successful_payment |
|---|
| 74 |
The join between orders and payments happens inside the subquery; o.user_id = u.id is still what ties it to each customer.
A condition inside the subquery
Any filter on the related rows goes inside the subquery. This lists the departments that have at least one employee earning 200,000 or more:
SELECT d.name, d.location
FROM departments d
WHERE EXISTS (SELECT 1
FROM employees e
WHERE e.department_id = d.id
AND e.salary >= 200000)
ORDER BY d.name;
▶ Run this query in the SQL Playground
| name | location |
|---|---|
| Customer Success | San Jose |
| Marketing | Chicago |
| Operations | Seattle |
EXISTS Inside CASE and SELECT
Because EXISTS produces true or false, it fits anywhere a condition does, including CASE WHEN. This labels each customer:
SELECT u.id, u.first_name,
CASE
WHEN EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'delivered') THEN 'Has a delivered order'
WHEN EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id) THEN 'Ordered, nothing delivered yet'
ELSE 'Never ordered'
END AS customer_state
FROM users u
ORDER BY u.id
LIMIT 8;
▶ Run this query in the SQL Playground
| id | first_name | customer_state |
|---|---|---|
| 1 | Jennifer | Has a delivered order |
| 2 | Donna | Has a delivered order |
| 3 | Kenneth | Never ordered |
| 4 | Michael | Has a delivered order |
| 5 | Robert | Has a delivered order |
| 6 | Deborah | Ordered, nothing delivered yet |
| 7 | Matthew | Ordered, nothing delivered yet |
| 8 | Timothy | Has a delivered order |
EXISTS vs IN, JOIN and COUNT
Several tools can answer "does a related row exist?". They differ in what they return and in how they treat duplicates and NULLs:
| Approach | Returns | Duplicates | Watch out for |
|---|---|---|---|
WHERE EXISTS (...) | Outer rows only | Never | Forgetting the correlation |
WHERE id IN (SELECT ...) | Outer rows only | Never | NOT IN returns nothing if the list has a NULL |
JOIN | Columns from both tables | One row per match | Needs DISTINCT for a yes/no question |
(SELECT COUNT(*) ...) > 0 | Outer rows only | Never | Counts every match even though one is enough |
The JOIN version shows the duplicate problem. Joining users to orders gives one row per order, not per customer:
SELECT (SELECT COUNT(*) FROM users u JOIN orders o ON o.user_id = u.id) AS join_rows,
(SELECT COUNT(*) FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)) AS exists_rows;
▶ Run this query in the SQL Playground
| join_rows | exists_rows |
|---|---|
| 200 | 114 |
Use EXISTS when you only need to filter, a JOIN when you need the other table's columns, and COUNT when the number itself is the answer. For a full comparison with IN, see IN vs EXISTS.
EXISTS and Performance
| Tip | Why |
|---|---|
Index the column the subquery matches on, such as orders(user_id) | Each existence check becomes an index lookup instead of a scan |
Prefer EXISTS over COUNT(*) > 0 | EXISTS can stop at the first match; COUNT has to count them all |
| Put extra filters inside the subquery | They narrow the rows the check has to look at |
| Check the plan | EXPLAIN (PostgreSQL, MySQL), EXPLAIN QUERY PLAN (SQLite) or the SQL Server execution plan shows whether the index is used |
See SQL indexes for how an index changes these lookups.
Common EXISTS Mistakes
Six errors, most of which run without any error message.
1. Missing the correlation
SELECT *
FROM users u
WHERE EXISTS (SELECT 1 FROM orders);
SELECT *
FROM users u
WHERE EXISTS (SELECT 1 FROM orders o
WHERE o.user_id = u.id);
Without a reference to the outer row, the subquery only asks whether orders has any rows at all, so every customer passes.
2. Using EXISTS when you need the related columns
SELECT u.first_name
FROM users u
WHERE EXISTS (SELECT 1 FROM orders o
WHERE o.user_id = u.id);
SELECT u.first_name, o.id AS order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id;
EXISTS only filters. To show anything from the other table, join it.
3. The same alias inside and outside
SELECT *
FROM users u
WHERE EXISTS (SELECT 1 FROM orders u
WHERE u.user_id = u.id);
SELECT *
FROM users u
WHERE EXISTS (SELECT 1 FROM orders o
WHERE o.user_id = u.id);
Inside the subquery the inner alias hides the outer one, so the condition compares two columns of the same order row.
4. COUNT(*) > 0 instead of EXISTS
WHERE (SELECT COUNT(*) FROM orders o
WHERE o.user_id = u.id) > 0
WHERE EXISTS (SELECT 1 FROM orders o
WHERE o.user_id = u.id)
Both are correct, but COUNT has to find every matching row before comparing with zero.
5. Expecting the selected column to filter
WHERE EXISTS (SELECT o.total FROM orders o
WHERE o.user_id = u.id)
WHERE EXISTS (SELECT 1 FROM orders o
WHERE o.user_id = u.id
AND o.total > 4000)
EXISTS never looks at what the subquery selects. Conditions on the related rows go in its WHERE clause.
6. NOT IN instead of NOT EXISTS
SELECT *
FROM employees
WHERE id NOT IN (SELECT manager_id FROM employees);
SELECT *
FROM employees e
WHERE NOT EXISTS (SELECT 1 FROM employees r
WHERE r.manager_id = e.id);
In the Playground the first query returns 0 rows because manager_id contains NULLs; the second returns the 110 employees who manage nobody.
EXISTS Cheat Sheet
| Task | Pattern |
|---|---|
| Has a related row | WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id) |
| Has no related row | WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id) |
| Related row with a condition | ... WHERE o.user_id = u.id AND o.status = 'delivered') |
| Through two tables | EXISTS (SELECT 1 FROM orders o JOIN payments p ON p.order_id = o.id WHERE o.user_id = u.id) |
| As a label | CASE WHEN EXISTS (...) THEN 'yes' ELSE 'no' END |
Practice EXISTS Queries
6 exercises on the real Playground tables.
Write each query yourself first. Every solution has been run against the Playground database.
-
1Easy Count the customers who have written at least one review. Table:
users, reviewsShow solution
SELECT COUNT(*) AS reviewers FROM users u WHERE EXISTS (SELECT 1 FROM reviews r WHERE r.user_id = u.id);▶ Run this query in the SQL Playground
The correlated subquery looks for one review per customer.
-
2Easy List the first 8 customers (by id) who have never ordered. Table:
users, ordersShow solution
SELECT u.id, u.first_name, u.last_name FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id) ORDER BY u.id LIMIT 8;▶ Run this query in the SQL Playground
NOT EXISTS keeps the users for whom the subquery finds no order.
-
3Medium List the products that have at least one 5-star review, cheapest first. Show 8. Table:
products, reviewsShow solution
SELECT p.name, p.price FROM products p WHERE EXISTS (SELECT 1 FROM reviews r WHERE r.product_id = p.id AND r.rating = 5) ORDER BY p.price LIMIT 8;▶ Run this query in the SQL Playground
The rating condition sits inside the subquery, next to the correlation.
-
4Medium Count the employees who manage at least one other employee. Table:
employeesShow solution
SELECT COUNT(*) AS managers FROM employees m WHERE EXISTS (SELECT 1 FROM employees e WHERE e.manager_id = m.id);▶ Run this query in the SQL Playground
The same table appears twice with different aliases: m for the possible manager, e for the people who might report to them.
-
5Medium Count the orders that have no payment with status "success". Table:
orders, paymentsShow solution
SELECT COUNT(*) AS unpaid_orders FROM orders o WHERE NOT EXISTS (SELECT 1 FROM payments p WHERE p.order_id = o.id AND p.status = 'success');▶ Run this query in the SQL Playground
This includes orders with no payment at all and orders whose payments all failed, are pending or were refunded.
-
6Hard Count the customers who have bought at least one product in the Books category. Table:
users, orders, order_items, productsShow solution
SELECT COUNT(*) AS book_buyers FROM users u WHERE EXISTS (SELECT 1 FROM orders o JOIN order_items oi ON oi.order_id = o.id JOIN products p ON p.id = oi.product_id WHERE o.user_id = u.id AND p.category = 'Books');▶ Run this query in the SQL Playground
The subquery joins three tables; only o.user_id = u.id connects it to the customer.
📝 Quiz — EXISTS
Test your understanding · 0/20 answered
What is EXISTS in SQL?
EXISTS is true or false; it never returns the subquery's data.
What does EXISTS return?
True if the subquery finds a row, false otherwise. SQLite shows them as 1 and 0.
Which query finds customers who have placed at least one order?
The correlated subquery checks for an order belonging to each user.
Why is SELECT 1 used inside EXISTS?
SELECT * behaves exactly the same; SELECT 1 is a readability convention.
Is the subquery inside EXISTS usually correlated?
The correlation is what makes the check specific to each outer row.
What advantage does NOT EXISTS have over NOT IN?
In the Playground, NOT IN against employees.manager_id returns 0 rows; NOT EXISTS returns 110.
How do you find employees who manage nobody?
NOT EXISTS is true when no one reports to that employee. manager_id IS NULL would find employees without a manager, a different question.
Can EXISTS stop at the first matching row?
This is called short-circuit evaluation.
What is the main difference between EXISTS and a JOIN?
In the Playground, joining users to orders gives 200 rows for 114 customers.
Why prefer EXISTS over COUNT(*) > 0?
Both give the same rows; EXISTS does less work.
How would you find students enrolled in at least one course?
The same pattern as customers and orders, on different tables.
What happens if the correlation condition is missing?
It then only asks whether the inner table has any matching rows at all.
Where is EXISTS usually written?
It can go anywhere a true/false condition is allowed.
How would you find customers with a successful payment, using customers, orders and payments?
The subquery can join tables while staying correlated to the outer customer.
Is EXISTS faster than IN in every case?
Check the plan with EXPLAIN on your own data.
What does NOT EXISTS usually find?
Customers with no orders, products never sold, and similar "missing" records.
How do you check whether an EXISTS query uses an index?
The plan shows whether the subquery does an index lookup or a full scan.
Can EXISTS be used without a subquery?
EXISTS is defined as a test on a subquery.
When should you use EXISTS rather than IN?
For short literal lists, IN is simpler.
Why is EXISTS a common interview topic?
The NOT IN versus NOT EXISTS difference is a favourite follow-up.
EXISTS Interview Questions
Q1. What does EXISTS do?
It returns true if its subquery produces at least one row and false if it produces none. It is normally correlated to the outer query, so the check runs for each outer row.
Q2. Why does it not matter what an EXISTS subquery selects?
EXISTS only checks whether a row is returned. SELECT 1, SELECT * and SELECT NULL all behave the same.
Q3. When is NOT EXISTS better than NOT IN?
Whenever the subquery column can contain NULL. A single NULL makes NOT IN return no rows, while NOT EXISTS is unaffected. It is a safe default for "has no match" questions.
Q4. EXISTS or JOIN?
EXISTS for filtering only, because it never duplicates outer rows. A JOIN when you need columns from the other table.
Q5. Why is EXISTS usually cheaper than COUNT(*) > 0?
One matching row is enough to make EXISTS true, so the search can stop there. COUNT has to find all matching rows before the comparison.
Q6. How do you make an EXISTS query fast?
Index the column the subquery matches on (for example orders.user_id), keep extra filters inside the subquery, and confirm with the query plan.
Frequently Asked Questions
What is EXISTS in SQL?
EXISTS is an operator that checks whether a subquery returns at least one row. It is true if the subquery finds any matching row and false otherwise, regardless of what that row contains.
How does EXISTS work?
For each row of the outer query, EXISTS runs the subquery with that row's values. As soon as the subquery finds one matching row, EXISTS is true and can stop looking; if it finds none, EXISTS is false.
What does EXISTS return?
A true or false value for each outer row. It never returns data from the subquery, only whether a matching row was found.
Is EXISTS faster than IN?
Sometimes, mainly on large related tables, because EXISTS can stop at the first match. Modern optimizers often give EXISTS and IN the same plan, so check with EXPLAIN on your own database.
Why use SELECT 1 in EXISTS?
EXISTS never looks at the selected columns, only at whether a row is returned. SELECT 1 makes that clear to the reader; SELECT * or SELECT NULL would behave the same.
Can EXISTS be used without a subquery?
No. EXISTS is defined as a test on the result of a subquery.
What is the difference between EXISTS and JOIN?
A JOIN combines columns from two tables and repeats an outer row once per match. EXISTS only checks whether at least one match exists and keeps each outer row at most once.
When should EXISTS be used?
Whenever you only need to know whether a related row exists, such as customers with at least one order or products with a 5-star review, and NOT EXISTS when you need the rows that have no match.