EXISTS and NOT EXISTS: Testing for Related Rows
Use EXISTS with a correlated subquery to find authors who have published at least one book, and NOT EXISTS for the opposite.
Overview
EXISTS tests whether a subquery returns any rows at all: it doesn't
care how many, or what their values are, only whether at least one row
matches. Paired with a correlated subquery (one that references a column
from the outer query), it answers questions like "does this author have at
least one book in the catalog?" without needing a JOIN and without the
risk of duplicating the outer row if multiple matches exist.
This is often a better fit than JOIN + DISTINCT for pure existence
checks, since EXISTS can stop scanning as soon as it finds one match and
never multiplies the outer row.
The Query
Step-by-Step Breakdown
FROM authors a: the outer query iterates over authors.WHERE EXISTS (SELECT 1 FROM books b WHERE b.author_id = a.id): for each author row, the subquery checks whether anybooksrow references that author'sid. The subquery is correlated:a.idcomes from the outer row currently being evaluated.SELECT 1: the actual selected value inside the subquery is irrelevant;EXISTSonly cares whether the subquery produces at least one row, soSELECT 1(orSELECT *) is a common convention.ORDER BY a.name: sort the surviving authors alphabetically.
In this sample dataset, every author has published at least one book, so
this query returns all ten authors, which is the expected, correct
behavior of EXISTS, not a sign the query is broken.
Variations
Flip the logic with NOT EXISTS to find authors with no books at all:
An empty result here is correct, not a bug
Because every author in this sample library dataset already has at least
one published book, NOT EXISTS correctly returns zero rows. Try adding a
new row to authors with an id that no books.author_id references,
you'll see that row appear in the NOT EXISTS results, confirming the
query behaves as expected.
Common Mistakes
- Selecting specific columns inside the subquery expecting them to matter.
EXISTSignores the subquery's selected columns entirely, only row presence matters, soSELECT 1,SELECT b.id, andSELECT *are all equivalent here. - Forgetting the correlation. If the subquery doesn't reference the
outer row (e.g.
WHERE b.author_id = 5instead of= a.id),EXISTSeither matches every outer row or none of them, since the subquery's result no longer depends on which author is being checked. - Using
EXISTSwhere aJOINwould duplicate rows anyway. If you actually need columns from the related table (not just a yes/no check), aJOINis usually more appropriate,EXISTSonly tells you a match exists, it can't return data from the matched row. - Confusing
NOT EXISTSwithNOT IN. They can behave differently whenNULLvalues are involved:NOT INagainst a subquery that returns anyNULLcan unexpectedly return zero rows for the whole query, whileNOT EXISTSdoesn't have this pitfall.
Cite this resource
SQLSimplified. "EXISTS and NOT EXISTS: Testing for Related Rows Example". Available at: https://sqlsimplified.online/examples/exists-operator