Postgres by Example: UNION, INTERSECT, EXCEPT
June 22, 2026 · View on GitHub
Set operations combine two result sets row-by-row. All three require the queries on either side to return the same number of columns with compatible types. UNION returns rows in either side (duplicates removed). INTERSECT returns rows in both. EXCEPT returns rows in the first that are not in the second. Each has an ALL variant that preserves duplicates and is faster because there is no deduplication step.
What you'll learn:
UNIONvsUNION ALLand when each is appropriateINTERSECTandINTERSECT ALLEXCEPTandEXCEPT ALL- Combining set operations and how precedence works
- Performance: when
UNION ALLis meaningfully faster
-- UNION removes duplicates across the combined result
SELECT 1 AS n UNION SELECT 2 UNION SELECT 1;
-- UNION ALL keeps duplicates (cheaper, no sort/hash)
SELECT 1 AS n UNION ALL SELECT 1 UNION ALL SELECT 2;
-- INTERSECT: rows present in both
SELECT id FROM fruits WHERE id <= 3
INTERSECT
SELECT id FROM fruits WHERE id >= 2;
-- EXCEPT: rows in the first query but not the second
SELECT id FROM fruits WHERE id <= 4
EXCEPT
SELECT id FROM fruits WHERE id >= 3;
-- Order of evaluation: INTERSECT binds tighter than UNION / EXCEPT.
-- Use parentheses to be explicit:
(SELECT id FROM fruits WHERE id < 5)
UNION
(SELECT id FROM fruits WHERE id > 100);
-- ORDER BY applies to the combined result; column refs by position or alias
SELECT id AS n FROM fruits WHERE id < 3
UNION
SELECT id FROM fruits WHERE id > 100
ORDER BY n;
UNION deduplicates by sort or hash on the full row — that work is real and shows up in EXPLAIN. If you know the inputs cannot duplicate (because the predicates are disjoint, for example), prefer UNION ALL. On large queries the difference can be substantial.
INTERSECT and EXCEPT are less common in application code but handy for analysis: "users who clicked A and B," "products in catalog A that are not in catalog B."
Set operations are set-of-rows operations. The column types must align across the inputs — int and text cannot be unioned directly. Names come from the first query's column names.
To run (requires fruits):
$ psql -f source/union-intersect-except.sql postgres
n
---
1
2
(2 rows)
n
---
1
1
2
(3 rows)
...
Common pitfalls:
UNIONinstead ofUNION ALLon queries that cannot produce duplicates wastes CPU on deduplication. PickUNION ALLunless duplicates are possible and unwanted.- Misaligned column counts or types:
SELECT id, name FROM t UNION SELECT id FROM t2is an error. Pad withNULL::textor align columns. ORDER BYafter a set operation refers to the combined result; it cannot use table-qualified column names. Use positions (ORDER BY 1) or aliases from the first query.EXCEPTis sensitive to NULL:EXCEPTtreats two NULLs as equal (likeDISTINCT), so a row with NULL on the left and NULL on the right is removed. This is usually what you want — but worth remembering.
Tip: When you find yourself writing SELECT ... WHERE x = 1 UNION ALL SELECT ... WHERE x = 2 UNION ALL ..., consider whether a single WHERE x IN (1, 2, 3) would do. Set operations are for genuinely different queries with results that need to be combined.
Try it: Write two SELECTs from fruits with different WHERE clauses and combine with UNION. Then change to UNION ALL and observe the duplicate count change (if any). Then try INTERSECT and EXCEPT between them.
Source: union-intersect-except.sql
Next: COUNT, SUM, AVG
Home: Postgres by Example