SQLab Hub
Lesson 21 · EXISTS
Lesson 21 · Subqueries

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.

∃ Checks row existence ▶ Runnable Playground examples 🧪 6 practice exercises 📝 20-question quiz ⏱ ~25 min

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:

SQLite · runs in the Playground
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

Customers with an order since July 2024
idfirst_namelast_name
4MichaelCarter
10NancyWilson
11RonaldWalker
22AnthonyHernandez
30RichardHarris
33MarkRobinson
✓ 6 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

EXISTS as a true/false value
any_order_over_4900any_order_over_10000
10
✓ 1 row · real output from the Playground database

EXISTS Syntax

SQL · syntax
SELECT columns
FROM outer_table t
WHERE EXISTS (SELECT 1
              FROM other_table o
              WHERE o.key = t.key);   -- the correlation
PartMeaning
EXISTS ( ... )True if the subquery returns at least one row
SELECT 1What the subquery selects does not matter; 1 just says "a row"
o.key = t.keyLinks the subquery to the current outer row. This makes it a correlated subquery
NOT EXISTS ( ... )True if the subquery returns no rows
SELECT 1, SELECT * or SELECT NULL? All three behave the same inside EXISTS, because the database only checks whether a row comes back. 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 rowSubquery findsEXISTSRow kept?
A customer with 3 orders3 rows (it can stop after the first)trueYes, once
A customer with 1 order1 rowtrueYes
A customer with no ordersNothingfalseNo

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.

SQLite · runs in the Playground
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

Products that have never been ordered
idnamecategory
110The Manager's PathBooks
✓ 1 row · real output from the Playground database

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:

SQLite · runs in the Playground
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 with orders and no cancellations or refunds
customers
74
✓ 1 row · real output from the Playground database

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:

SQLite · runs in the Playground
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 a successful payment
customers_with_successful_payment
74
✓ 1 row · real output from the Playground database

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:

SQLite · runs in the Playground
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

Departments with a top earner
namelocation
Customer SuccessSan Jose
MarketingChicago
OperationsSeattle
✓ 3 rows · real output from the Playground database

EXISTS Inside CASE and SELECT

Because EXISTS produces true or false, it fits anywhere a condition does, including CASE WHEN. This labels each customer:

SQLite · runs in the Playground
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

Customers labelled with EXISTS
idfirst_namecustomer_state
1JenniferHas a delivered order
2DonnaHas a delivered order
3KennethNever ordered
4MichaelHas a delivered order
5RobertHas a delivered order
6DeborahOrdered, nothing delivered yet
7MatthewOrdered, nothing delivered yet
8TimothyHas a delivered order
✓ 8 rows · real output from the Playground database

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:

ApproachReturnsDuplicatesWatch out for
WHERE EXISTS (...)Outer rows onlyNeverForgetting the correlation
WHERE id IN (SELECT ...)Outer rows onlyNeverNOT IN returns nothing if the list has a NULL
JOINColumns from both tablesOne row per matchNeeds DISTINCT for a yes/no question
(SELECT COUNT(*) ...) > 0Outer rows onlyNeverCounts 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:

SQLite · runs in the Playground
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

Rows from a JOIN and from EXISTS
join_rowsexists_rows
200114
✓ 1 row · real output from the Playground database

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

TipWhy
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(*) > 0EXISTS can stop at the first match; COUNT has to count them all
Put extra filters inside the subqueryThey narrow the rows the check has to look at
Check the planEXPLAIN (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

❌ True for every customer
SQL
SELECT *
FROM users u
WHERE EXISTS (SELECT 1 FROM orders);
✅ Checks each customer
SQL
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

❌ Cannot show the order total
SQL
SELECT u.first_name
FROM users u
WHERE EXISTS (SELECT 1 FROM orders o
              WHERE o.user_id = u.id);
✅ JOIN returns it
SQL
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

❌ Compares orders with itself
SQL
SELECT *
FROM users u
WHERE EXISTS (SELECT 1 FROM orders u
              WHERE u.user_id = u.id);
✅ Distinct aliases
SQL
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

❌ Counts every match
SQL
WHERE (SELECT COUNT(*) FROM orders o
       WHERE o.user_id = u.id) > 0
✅ Stops at the first
SQL
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

❌ The column is ignored
SQL
WHERE EXISTS (SELECT o.total FROM orders o
              WHERE o.user_id = u.id)
✅ Filter in WHERE
SQL
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

❌ Empty result if the list has a NULL
SQL
SELECT *
FROM employees
WHERE id NOT IN (SELECT manager_id FROM employees);
✅ Always correct
SQL
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

TaskPattern
Has a related rowWHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
Has no related rowWHERE 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 tablesEXISTS (SELECT 1 FROM orders o JOIN payments p ON p.order_id = o.id WHERE o.user_id = u.id)
As a labelCASE 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.

  1. 1Easy Count the customers who have written at least one review. Table: users, reviews

    Show solution
    SQLite · runs in the Playground
    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.

  2. 2Easy List the first 8 customers (by id) who have never ordered. Table: users, orders

    Show solution
    SQLite · runs in the Playground
    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.

  3. 3Medium List the products that have at least one 5-star review, cheapest first. Show 8. Table: products, reviews

    Show solution
    SQLite · runs in the Playground
    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.

  4. 4Medium Count the employees who manage at least one other employee. Table: employees

    Show solution
    SQLite · runs in the Playground
    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.

  5. 5Medium Count the orders that have no payment with status "success". Table: orders, payments

    Show solution
    SQLite · runs in the Playground
    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.

  6. 6Hard Count the customers who have bought at least one product in the Books category. Table: users, orders, order_items, products

    Show solution
    SQLite · runs in the Playground
    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

Question 1 of 20

What is EXISTS in SQL?

Question 2 of 20

What does EXISTS return?

Question 3 of 20

Which query finds customers who have placed at least one order?

Question 4 of 20

Why is SELECT 1 used inside EXISTS?

Question 5 of 20

Is the subquery inside EXISTS usually correlated?

Question 6 of 20

What advantage does NOT EXISTS have over NOT IN?

Question 7 of 20

How do you find employees who manage nobody?

Question 8 of 20

Can EXISTS stop at the first matching row?

Question 9 of 20

What is the main difference between EXISTS and a JOIN?

Question 10 of 20

Why prefer EXISTS over COUNT(*) > 0?

Question 11 of 20

How would you find students enrolled in at least one course?

Question 12 of 20

What happens if the correlation condition is missing?

Question 13 of 20

Where is EXISTS usually written?

Question 14 of 20

How would you find customers with a successful payment, using customers, orders and payments?

Question 15 of 20

Is EXISTS faster than IN in every case?

Question 16 of 20

What does NOT EXISTS usually find?

Question 17 of 20

How do you check whether an EXISTS query uses an index?

Question 18 of 20

Can EXISTS be used without a subquery?

Question 19 of 20

When should you use EXISTS rather than IN?

Question 20 of 20

Why is EXISTS a common interview topic?

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.