Up to this point, every query you’ve written has answered one question against one logical result set. This file introduces a different kind of question: “How do two or more result sets relate to each other?” That is the question set operators answer.
A set operator takes two (or more) SELECT statements that produce the same shape of result — same number of columns, compatible data types — and combines them according to set theory:
| Operator | Meaning | Analogy |
|---|---|---|
UNION |
Rows in A, or in B, or in both — no duplicates | Merge two mailing lists, dedup |
UNION ALL |
Rows in A, then rows in B — duplicates kept | Stack two mailing lists |
INTERSECT |
Rows in both A and B | Find the overlap |
EXCEPT / MINUS |
Rows in A that are not in B | Find what’s missing |
Each SELECT in the combination is called a branch. The database evaluates each branch independently, then applies the set operation across the combined output.
SQL was designed around the relational model, and the relational model is built on set theory — a table is a set of rows. It follows naturally that the language needs operators for the fundamental set operations: union, intersection, and difference. Without them, comparing two tables would require awkward, error-prone workarounds involving temporary tables and procedural loops. Set operators let you express “combine,” “overlap,” and “difference” directly, in one statement, in a way the query optimizer can plan efficiently.
Businesses rarely keep all their data in one table. A national retailer has regional sales tables. A company that just completed a merger has two HR systems. A finance team keeps last year’s numbers in an archive table and this year’s in a live one. Any time the business is structured as “the same kind of data, split across multiple places,” a set operator is likely the correct tool — not a JOIN, which combines different kinds of data side-by-side.
all_customers view using UNION ALL.intake_2024 and intake_2025 systems to find continuing patients (INTERSECT).expected_shipments against received_shipments to find what never arrived (EXCEPT).EXCEPT both directions should return zero rows). Table A Table B
┌─────────┐ ┌─────────┐
│ 1 2 │ │ 2 3 │
│ 3 │ │ │
└─────────┘ └─────────┘
UNION → { 1, 2, 3 } (distinct, combined)
UNION ALL → { 1, 2, 3, 2, 3 } (all rows, combined)
INTERSECT → { 2, 3 } (in both)
EXCEPT (A-B) → { 1 } (in A, not in B)
EXCEPT (B-A) → { } (in B, not in A — B ⊆ A here)
-- General shape
SELECT column_list FROM table_a WHERE ...
UNION | UNION ALL | INTERSECT | EXCEPT
SELECT column_list FROM table_b WHERE ...
[ORDER BY column_name]; -- applies to the FINAL combined result only
Three rules govern every set operator, regardless of vendor:
INT to DECIMAL are usually fine; VARCHAR to DATE typically is not, or is dangerously silent).emp_name and branch B’s is aliased dept_name, the combined result set is named emp_name — a common source of confusion for reviewers.Positional matching — not name matching — governs which columns line up. SELECT a, b FROM x UNION SELECT c, d FROM y matches a with c and b with d, regardless of what they’re called.
-- One combined list of every city where the company has either an office or a warehouse
SELECT city FROM office_locations
UNION
SELECT city FROM warehouse_locations;
-- Every ID currently in use across two identifier pools, deduplicated
SELECT employee_id AS company_id FROM employees
UNION
SELECT contractor_id AS company_id FROM contractors;
UNION vs UNION ALL deliberately (covered fully in 02_UNION_AND_UNION_ALL.md).ORDER BY once, at the very end.A UNION B UNION C) is valid and common; the operator is left-associative but functionally behaves the same as repeated pairwise combination.A UNION B EXCEPT C) follows operator precedence rules that vary slightly by vendor — use parentheses to make intent explicit rather than relying on default precedence.UNION, INTERSECT, and EXCEPT all require the engine to detect duplicates, which typically means a sort or hash-based deduplication step over the full combined result. UNION ALL skips this entirely and is materially cheaper on large datasets. This single fact is the most important performance lesson in this module and is expanded fully in 06_PERFORMANCE_AND_OPTIMIZATION.md.
| Feature | MySQL | PostgreSQL | SQL Server | Oracle |
|---|---|---|---|---|
UNION / UNION ALL |
✅ | ✅ | ✅ | ✅ |
INTERSECT |
8.0.31+ | ✅ | ✅ | ✅ |
EXCEPT |
8.0.31+ | ✅ | ✅ | MINUS |
| Parentheses around branches | ✅ | ✅ | ✅ | ✅ |
ORDER BY can only appear once, at the end of the whole statement.NULL values are treated as equal to each other for deduplication purposes in UNION/INTERSECT/EXCEPT — two NULL rows are considered duplicates and collapsed, which differs from NULL’s usual “unknown, never equal” behavior in WHERE clauses.UNION and UNION ALL?UNION result come from the first SELECT?NULL behave differently in UNION deduplication versus a WHERE clause comparison?UNION on a large table pass code review but fail a production performance SLA?Set operators combine result sets of the same shape using the logic of set theory: union, intersection, and difference. The rules — matching column counts, compatible types, and first-branch naming — are simple, but the engineering judgment about which operator and at what cost is where real skill shows.
dept_name in departments and every city in locations.emp_id values and all manager_id values from employees, without duplicates.SELECT emp_name FROM employees UNION SELECT dept_id FROM departments would fail or behave incorrectly.UNION, INTERSECT, MINUS