SQL NOT EXISTS: find rows without a matching record
SQL NOT EXISTS returns rows for which a correlated subquery finds no matching record. This anti join pattern is useful for customers without orders, products without sales, empty groups, and other missing-relationship checks.
Updated · SQL Arena team
What you will practice
- Find unmatched rows with NOT EXISTS
- Write a correctly correlated subquery
- Express the same anti join with LEFT JOIN ... IS NULL
- Avoid NOT IN problems caused by NULL
- Check for absence without unnecessary GROUP BY
A practice problem in the trainer
This is how a problem looks in SQL Arena: the task, the editor, the result and the database schema.
Return customers from customers for whom no row exists in orders with the same customer_id. Output only customer_name and city.
SELECT c.customer_name, c.city FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id ) ORDER BY c.customer_name;
| # | customer_name | city |
|---|---|---|
| 1 | Ben | York |
| 2 | Dylan | Leeds |
| 3 | Felix | Bath |
Return customers from customers for whom no row exists in orders with the same customer_id. Output only customer_name and city.
SELECT c.customer_name, c.city FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id ) ORDER BY c.customer_name;
How SQL NOT EXISTS works as an anti join
The outer query scans customers. For each current customer row, PostgreSQL evaluates the subquery inside NOT EXISTS. The condition o.customer_id = c.id correlates that inner check with the current customer. SELECT 1 is conventional because EXISTS only cares whether at least one row is found; it does not use the selected value.
When a matching order exists, EXISTS is true and NOT EXISTS becomes false, so that customer is removed. When no matching order exists, the subquery returns no rows and the customer remains. In this dataset Ava, Cara, Emma, and Grace have orders, leaving Ben, Dylan, and Felix in the result.
You can express the same logic with LEFT JOIN orders o ON o.customer_id = c.id followed by WHERE o.id IS NULL. That is also an anti join. NOT EXISTS often reads closer to the requirement, while LEFT JOIN ... IS NULL can fit naturally into a query that already uses joins. NOT IN, however, needs extra care because a NULL returned by its subquery can change the truth value of the comparison.
EXISTS is true when its subquery finds at least one matching row, while NOT EXISTS reverses that result. The expression selected inside the subquery is usually irrelevant because these operators test row existence rather than returned values. In a correlated anti join, a condition such as o.customer_id = c.id connects the inner query to the current outer row. If a match is found, NOT EXISTS is false; if no match is found, the outer row remains in the result.
SELECT c.customer_name, c.city FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id ) ORDER BY c.customer_name;
| customer_name | city |
|---|---|
| Ben | York |
| Dylan | Leeds |
| Felix | Bath |
Common SQL NOT EXISTS mistakes
Forgetting the correlation
If the subquery does not reference c.id, it checks whether any order exists anywhere. As soon as orders contains one row, NOT EXISTS is false for every customer.
Using NOT IN with nullable data
c.id NOT IN (SELECT customer_id FROM orders) can behave unexpectedly when the subquery contains NULL. Either filter NULL explicitly or use NOT EXISTS for a clearer absence check.
Testing a nullable joined column
With LEFT JOIN ... IS NULL, test a right-side column that cannot be NULL when a match exists, such as the primary key o.id. Testing a nullable business column can confuse “no joined row” with “joined row containing NULL”.
Practice problems on this topic
SQL for QA testers trackOnce the basic customer-order pattern is clear, practice the same logic on different schemas. Exercises such as products nobody bought or books with no available copies make the anti join pattern automatic. Medium tasks add conditions such as a year or category, forcing you to place filters on the correct side of the absence test. For data validation, continue with the “SQL for QA Engineers” track. SQL Arena has 1,192 tasks, with answers checked on hidden data rather than only the visible sample.
- EasyfreeCustomers who never placed an orderpostgresqlclickhouse
- EasyfreeProducts that nobody boughtpostgresqlclickhouse
- EasyfreeBooks Out of Stockpostgresqlmysqlclickhouse
- MediumPremiumCustomers Without Purchasespostgresqlmysqlclickhouse
- MediumPremiumEmpty Sectionspostgresqlmysqlclickhouse
- MediumPremiumBooks not loaned in 2005postgresqlmysqlclickhouse
- MediumPremiumGenres with no loans in 2005postgresqlmysqlclickhouse
- MediumPremiumSalespeople Who Never Sold to REDpostgresqlmysqlclickhouse
Reference on this topic
In-depth articles with syntax and examples in the SQL Arena reference.
Frequently asked questions
What is SQL NOT EXISTS?
SQL NOT EXISTS is a condition that becomes true when its subquery returns no rows. It is commonly used to find records that do not have a related record in another table.
How does SQL NOT EXISTS work?
The subquery usually contains a condition connecting it to the current outer row. If the subquery finds a match, NOT EXISTS is false; if it finds no match, NOT EXISTS is true.
How do you use SQL NOT EXISTS for an anti join?
Select rows from the main table and place a correlated NOT EXISTS subquery in the WHERE clause. The correlation checks for a related row in the second table, and only unmatched outer rows are retained.
What is the difference between SQL NOT EXISTS and EXISTS?
EXISTS is true when at least one matching row is found. NOT EXISTS is true only when no matching row is found.
What is the difference between SQL NOT EXISTS and LEFT JOIN?
Both can express an anti join. A LEFT JOIN version normally filters for NULL on the joined side, whereas NOT EXISTS directly states that no matching row may exist.
Practice SQL NOT EXISTS and anti joins
Use SQL Arena to solve anti join exercises across different schemas and data shapes. The free plan includes 292 tasks for PostgreSQL, MySQL, ClickHouse, and BigQuery with no card required.
Practice anti joinsMore practice from CodeRang
- Python ArenaPython Arena has 1300+ Python 3.12 problems covering functions, classes, async code and technical interview tasks.1300+ tasks · Python 3.12
- Playwright ArenaPlaywright Arena has 30 JavaScript and E2E problems where you write a test and immediately see what it did in the browser.30 tasks · JavaScript / E2E