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
Loading database engine...
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:
Loading database engine...
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.