37 EXISTS and Derived Tables
EXISTS (subquery) is true when the subquery returns at least one row. It is the workhorse of “does any related row satisfy X?” queries — usually written as a correlated subquery referencing the outer row. A derived table is a subquery in the FROM clause, given an alias and used like a table. The two together cover most of the SQL you write before you reach CTEs and window functions.
What you’ll learn
EXISTSandNOT EXISTS: tests for “any row exists” / “no row exists”- Why
NOT EXISTSbeatsNOT INfor nullable subqueries - Derived tables: a subquery in
FROMwith an alias - When a derived table is clearer than a CTE
- The relationship to
INand joins
Example
The convention SELECT 1 inside EXISTS is not magic — PostgreSQL does not care what columns the subquery returns; it only checks whether any row would be produced. SELECT 1, SELECT *, SELECT NULL all work the same. SELECT 1 reads as “I do not care about the columns” and is the conventional choice.
A derived table — also called an inline view — is just a subquery in FROM. An explicit alias (after the closing parenthesis) gives the result a name you can refer to. Derived tables compose well; you can join them, filter them, group them, even nest them. The main downside is that a complex derived table buries the logic in the middle of the query. CTEs (next lesson) are usually a nicer way to name an intermediate result.
Run the example
To run (requires customers_example and orders_example):
Common pitfalls
IN (SELECT col FROM t)andEXISTS (SELECT 1 FROM t WHERE t.col = outer.col)are usually equivalent — butNOT INandNOT EXISTSare not when NULLs are possible. PreferNOT EXISTSwhenever NULLs are on the table.- An alias such as
AS xis useful for clarity and portability. PostgreSQL 16 and later allow subqueries inFROMwithout one; older PostgreSQL versions require an alias. - A derived table with
ORDER BYinside is allowed but the order is not preserved when the outer query consumes it — sort outside. - Some old guides said
EXISTSis “always slower thanIN.” On modern PostgreSQL the planner handles both well; choose for correctness and readability first.
EXISTS short-circuits — once it finds the first matching row, it stops. So WHERE EXISTS (SELECT 1 FROM big_table WHERE ...) can be very fast if the matching row is found early.
Find fruits that have a name appearing exactly once: SELECT name FROM fruits f WHERE NOT EXISTS (SELECT 1 FROM fruits f2 WHERE f2.name = f.name AND f2.id <> f.id);. Then rewrite a derived table as a CTE in your head (preview of the next lesson): WITH spend AS (SELECT customer_id, sum(total) AS total_spent FROM orders_example GROUP BY customer_id) SELECT * FROM spend WHERE total_spent > 30;.
Downloads
SQL file: exists-and-derived-tables.sql · All lesson files · Setup guide