19. Set operations: UNION, INTERSECT, EXCEPT
A different kind of combining
Every join so far combines tables side by side — matching rows, producing wider results with columns from both sides. Set operations combine query results vertically — stacking or comparing two result sets with the same shape (same number of columns, compatible types), producing a result with the same width as either input, just a different set of rows.
All three set operations require both queries to return the same number of columns, with compatible (implicitly castable) types in the same positions — column names don't need to match; the final result takes its column names from the first query.
UNION and UNION ALL
UNION combines two result sets and removes duplicate rows (a full
row must match exactly to count as a duplicate — same rule as DISTINCT
from Module 1). UNION ALL combines them and keeps every row,
duplicates included.
-- Every distinct city name that's either a customer's city OR appears in
-- the address book at all — duplicates collapsed.
SELECT city FROM city WHERE country_id = 1
UNION
SELECT city FROM city WHERE country_id = 2;
Default to UNION ALL unless you specifically need deduplication.
UNION's duplicate removal isn't free — like DISTINCT, it requires
comparing rows (typically a sort or hash), real cost on a large result
set. If you already know the two sources can't overlap (common — e.g.
combining "active customers" and "inactive customers," which are
mutually exclusive by construction), UNION ALL gives the identical
result faster, with no wasted work:
-- Both branches are mutually exclusive by the WHERE conditions — UNION ALL
-- is strictly correct here AND faster than UNION, since there's nothing
-- to deduplicate.
SELECT customer_id, 'active' AS status FROM customer WHERE active = 1
UNION ALL
SELECT customer_id, 'inactive' AS status FROM customer WHERE active = 0;
INTERSECT
Returns only rows that appear in both result sets:
-- Customers who exist in BOTH a "rented an R-rated film" list AND a
-- "rented a G-rated film" list — i.e. customers who've rented both kinds.
SELECT customer_id FROM rental r JOIN inventory i ON i.inventory_id = r.inventory_id
JOIN film f ON f.film_id = i.film_id WHERE f.rating = 'R'
INTERSECT
SELECT customer_id FROM rental r JOIN inventory i ON i.inventory_id = r.inventory_id
JOIN film f ON f.film_id = i.film_id WHERE f.rating = 'G';
This is a genuinely different question than a JOIN or WHERE ... AND
could answer directly — you're not filtering rows of one query, you're
comparing the membership of two independent result sets. INTERSECT
is equivalent to (and often rewritten by the planner into) a
semi-join pattern using EXISTS, which you'll meet properly in Module 6.
EXCEPT
Returns rows in the first result set that do not appear in the second — set subtraction, order matters:
-- Customers who've rented a G-rated film but NEVER an R-rated one.
SELECT customer_id FROM rental r JOIN inventory i ON i.inventory_id = r.inventory_id
JOIN film f ON f.film_id = i.film_id WHERE f.rating = 'G'
EXCEPT
SELECT customer_id FROM rental r JOIN inventory i ON i.inventory_id = r.inventory_id
JOIN film f ON f.film_id = i.film_id WHERE f.rating = 'R';
(MySQL historically calls this MINUS-adjacent territory differently —
Oracle uses MINUS as the keyword for exactly this operation; Postgres
and the SQL standard both use EXCEPT. Filed for recognition, not
something to worry about while writing Postgres.)
Set operations vs. JOIN — when to reach for which
A rule of thumb: if you're combining rows from the same conceptual
entity under different conditions (union), or comparing membership
between two groups (intersect/except), reach for set operations. If
you're pulling related-but-different columns together side by side
(a customer's name next to their rental date), that's a JOIN. The two
are not interchangeable — INTERSECT on customer IDs and JOIN on
customer IDs answer genuinely different questions, and it's worth pausing
to identify which one you actually mean before writing either.
Check yourself
- What has to be true about two queries' result shapes before you can
UNIONthem? - Why is
UNION ALLthe better default when you already know the two sources can't overlap? - Rewrite "customers who've rented an R film but never a G film" using
EXCEPT, and explain in one sentence why order matters forEXCEPTbut not forUNIONorINTERSECT.