UNION, INTERSECT, and EXCEPT
Combine result sets vertically — stack rows from multiple queries, find common rows, subtract one set from another, and every rule about column matching, deduplication, and ordering
Quick answer
Set operations stack the results of two queries: UNION combines them without duplicates, UNION ALL keeps duplicates, INTERSECT keeps rows in both, and EXCEPT keeps rows in the first but not the second.
- Both queries need the same number of columns, with compatible types.
- UNION ALL is faster; use UNION only when you need duplicates removed.
- ORDER BY goes once, at the end, and sorts the combined result.
SELECT city FROM customers
INTERSECT
SELECT city FROM stores
ORDER BY city;Result · 7 rows
| city |
|---|
| Austin |
| Chicago |
| Los Angeles |
| New York |
| Phoenix |
| Portland |
| Seattle |
// Part 01
Set Operations — Combining Queries Vertically
Every SQL operation you have learned — JOINs, subqueries, aggregates — combines data horizontally, adding columns. Set operations combine data vertically, stacking rows from multiple queries on top of each other. The result has the same columns as each input query but more (or fewer) rows.
SQL provides three set operations, each from classical set theory:
The fundamental rule — columns must match
All set operations require the same number of columns in the same order with compatible data types across all queries being combined. Column names in the result come from the first query.
// Part 02
UNION vs UNION ALL — Deduplication Cost
UNION removes duplicate rows — any row that appears in both queries appears only once in the result. UNION ALL keeps every row from both queries, including duplicates. UNION ALL is almost always the right choice when you know the result sets are disjoint (no overlapping rows) — it is significantly faster because it skips the deduplication sort.
// Part 03
UNION ALL — The Workhorse of Vertical Combination
UNION ALL is the most used set operation in production SQL. It stacks rows from multiple queries without deduplication — exactly what you need when combining disjoint data sources, multiple time periods, or labelled segments.
Combining multiple customer segments into one list
Combining data from multiple periods
Combining different tables with compatible structures
// Part 04
UNION for Report Subtotals — Combining Detail and Summary
A classic UNION ALL pattern in reporting: combine detail rows with summary rows. The result looks like a hierarchical report — individual records at the top (or bottom) with a total row appended.
// Part 05
INTERSECT — Finding Common Rows
INTERSECT returns only rows that appear in both result sets. It is the SQL equivalent of set intersection — A ∩ B. Rows unique to either side are excluded. INTERSECT always deduplicates.
Customers who have placed both delivered AND cancelled orders
Products sold in multiple specific stores
INTERSECT vs INNER JOIN — when each is appropriate
// Part 06
EXCEPT — Subtracting One Set From Another
EXCEPT (called MINUS in Oracle and some other databases) returns rows from the first query that do not appear in the second query. It is the set difference — A − B. Rows unique to the second query are discarded. EXCEPT always deduplicates.
Customers who ordered but never cancelled
Products sold in January but not in February
EXCEPT vs NOT EXISTS vs LEFT JOIN IS NULL
// Part 07
Ordering and LIMIT With Set Operations
ORDER BY and LIMIT apply to the entire result of the set operation — not to individual queries. They must appear after the final query in the set operation chain. Attempting to add ORDER BY to an individual query within a UNION causes an error in most databases.
// Part 08
Practical Patterns — Set Operations in Production Analytics
Full UNION pipeline report — all order statuses in one view
The query below opens with a CTE (the WITH clause) — you'll learn CTEs properly in Lesson 55; for now, just know it names a subquery so it can be referenced by name later in the query.
Change detection — what changed between two snapshots
Finding symmetric difference with EXCEPT + UNION ALL
// Part 09
What This Looks Like at Work
You are a data engineer at Square. Every morning at 7 AM, the reconciliation job produces a report comparing yesterday's transaction data across three systems: the primary OLTP database, the data warehouse, and the partner bank's settlement file. Each source must agree. You use EXCEPT and UNION ALL to identify discrepancies.
🎯 Pro Tip
Reconciliation reports are one of the highest-value use cases for UNION ALL + EXCEPT. The pattern: (1) define each source as a CTE, (2) EXCEPT to find rows exclusive to each source, (3) UNION ALL to combine matched and exclusive rows with a status label. This architecture handles any number of sources — add one more CTE and one more UNION ALL block.
// Part 10
Interview Prep — 5 Questions With Complete Answers
Both UNION and UNION ALL combine the result sets of two or more queries by stacking rows vertically. The difference is deduplication. UNION removes duplicate rows — if the same row appears in both queries, it appears only once in the final result. UNION ALL keeps all rows from all queries, including duplicates — if the same row appears in both queries, it appears twice.
The performance implication is significant. UNION must sort or hash all rows to identify and remove duplicates — O(n log n) work. UNION ALL simply appends result sets — O(n) work. UNION ALL is always faster than UNION. On large result sets, this difference can be substantial.
When to use each: use UNION ALL when you know the result sets are disjoint (no possible overlapping rows) — different time periods, different status values, different segments. Use UNION when the same row could appear in multiple queries and you want each unique row once — combining a current customer list with a historical list where some customers appear in both. The common mistake is defaulting to UNION (with deduplication) when UNION ALL is correct and faster. Always ask: "can the same row appear in both queries?" If yes and you want it once, use UNION. If yes and you want it multiple times, or if it is impossible for the same row to appear twice, use UNION ALL.
Three requirements must be satisfied for any set operation (UNION, UNION ALL, INTERSECT, EXCEPT) to execute successfully. First: all queries must return the same number of columns. A SELECT with 3 columns cannot be combined with a SELECT with 4 columns — the column count must match exactly across all queries in the set operation.
Second: the corresponding columns must have compatible data types. Column 1 of query A must be compatible with column 1 of query B — both numeric, both text, both date, etc. Incompatible types (combining a DATE column with an INTEGER column in the same position) cause a type error. Most databases will attempt implicit type casting for closely related types (INTEGER and DECIMAL are usually compatible) but will error on clearly incompatible combinations (DATE and VARCHAR).
Third: ORDER BY and LIMIT must appear after the final query and apply to the entire combined result — not to individual queries within the set operation. Adding ORDER BY to an individual query within UNION causes a syntax error in most databases (PostgreSQL, MySQL 8+). If you need to sort or limit individual queries before combining, wrap each query in a subquery: SELECT * FROM (SELECT ... ORDER BY ... LIMIT n) AS q1 UNION ALL SELECT * FROM (SELECT ... ORDER BY ... LIMIT n) AS q2. The column names in the result come from the first query — if query A has column named "customer_name" and query B has column named "emp_name", the result column is named "customer_name" throughout.
INTERSECT finds rows that appear in both result sets simultaneously — it operates on complete result rows, not on join keys. It is the natural choice when you want to find entities that satisfy two independent conditions that can be expressed as separate queries, and you want the common elements.
INTERSECT is appropriate when: the two queries are logically independent (neither is a "lookup" of the other), both queries naturally return the same columns, and you want the rows that appear in both. Example: customer_ids who placed delivered orders INTERSECT customer_ids who placed cancelled orders — these are two independent queries against the same table, and INTERSECT cleanly finds customers who appear in both.
JOIN is appropriate when you want to combine columns from different tables based on a relationship — horizontally expanding the result. INTERSECT is a vertical set operation — it does not add columns, it finds common rows. A JOIN of customers to orders adds order columns to customer rows. An INTERSECT of delivered customer_ids and cancelled customer_ids finds customer_ids that appear in both sets — no additional columns. When to prefer INTERSECT over a JOIN + filter: when both conditions are expressed on the same table with different WHERE clauses (same column, different values), INTERSECT is more direct. When you need columns from both result sets, EXISTS or JOIN is more appropriate than INTERSECT because INTERSECT does not give you access to the second query's specific rows.
All three find rows in one set that are absent from another, but they work at different levels of abstraction and have different capabilities. EXCEPT operates on complete result sets — it compares entire rows across two queries and returns rows from the first query that are not present in the second. It always deduplicates. It works cleanly when both queries return the same columns naturally.
NOT IN operates on a single column — it filters outer rows where a specific column's value does not appear in a list returned by the subquery. NOT IN fails silently when the subquery contains NULL values. NOT EXISTS is a correlated subquery that checks row-by-row whether a related record exists — it is NULL-safe and can handle complex multi-column or multi-table conditions in the subquery.
Choose based on the problem structure: EXCEPT when both sides are naturally expressed as full queries returning the same columns — "customer_ids who ordered in January" EXCEPT "customer_ids who ordered in February" is a natural set difference. NOT EXISTS when the anti-join condition is complex — involves multiple tables, multiple conditions, or you need flexibility beyond what two parallel queries provide. NOT IN when the subquery is simple, small, and guaranteed NULL-free. EXCEPT is cleanest for comparing two entire queries; NOT EXISTS is most flexible for row-level anti-join with complex conditions. Performance: all three produce anti-join plans in the optimiser for most databases — performance is typically equivalent when proper indexes exist.
Set operations treat NULL values as equal to each other for the purpose of deduplication and comparison — unlike standard SQL WHERE comparisons where NULL ≠ NULL. This is a specific exception to NULL's usual semantics and it is important for understanding UNION, INTERSECT, and EXCEPT behaviour.
In UNION (with deduplication): if both queries return a row where a specific column is NULL, the NULL row appears only once in the result — the database treats the two NULL values as "equal" for deduplication. In INTERSECT: a row with NULL in one query can match a row with NULL in the other query — NULLs are considered equal. In EXCEPT: a row with NULL in the first query is considered matched by a row with NULL in the second query and is therefore excluded from the EXCEPT result.
This NULL-equality behaviour in set operations is defined by the SQL standard and implemented consistently in PostgreSQL, MySQL 8+, and SQL Server. It differs from WHERE NULL = NULL (which evaluates to NULL, not TRUE) and is closer to IS NOT DISTINCT FROM semantics. Practical implication: if you are using EXCEPT to find "new" rows and some rows have NULL keys, those NULL-key rows from the first query will be considered matched by NULL-key rows from the second query and excluded from EXCEPT — which is usually the desired behaviour. When you specifically need NULL to not match NULL in a set operation context, you need a more complex approach using NOT EXISTS with IS NULL checks.
// Part 11
Errors You Will Hit — And Exactly Why They Happen
🎯 Key Takeaways
- ✓Set operations combine query results vertically — stacking rows. JOINs combine horizontally — adding columns. They solve different problems.
- ✓Three set operations: UNION (all rows, deduplicates), INTERSECT (common rows only), EXCEPT (first minus second). All require matching column count and compatible types.
- ✓UNION ALL skips deduplication — O(n) vs UNION's O(n log n). Default to UNION ALL when result sets are disjoint or duplicates are acceptable. Only use UNION when you need deduplication across sources.
- ✓Column names in the result come from the first query. All subsequent queries contribute rows but their column names are ignored.
- ✓ORDER BY and LIMIT apply to the entire combined result and must appear after the final query. To sort/limit individual queries, wrap each in a subquery.
- ✓INTERSECT finds rows that satisfy two independent conditions — entities that appear in both result sets. NULL = NULL for set operation comparisons (unlike standard WHERE).
- ✓EXCEPT returns rows from the first query not present in the second. Equivalent to NOT EXISTS and LEFT JOIN IS NULL for set differences — choose based on complexity of conditions.
- ✓Set operations always deduplicate (except UNION ALL). UNION deduplicates across both queries. INTERSECT and EXCEPT deduplicate by definition.
- ✓Symmetric difference (rows unique to either side): (A EXCEPT B) UNION ALL (B EXCEPT A). This finds everything that does not overlap.
- ✓Reconciliation reports are the primary production use case: EXCEPT to find missing records, UNION ALL to combine matched and unmatched sets with status labels into one complete reconciliation view.
What comes next
In Lesson 40, you learn derived tables in depth — subqueries in FROM as building blocks for multi-step analytics, the rules for aliasing and referencing, and when a derived table beats a CTE.
Lesson 40 → Derived TablesQuick check
Which keeps duplicate rows?
HELP US IMPROVE
How was this lesson?
Your note is shared with the Chaduvuko team for review. Please don’t include personal or sensitive information.
Spotted a mistake or something unclear? Suggest a fix on GitHub or email us. Select the text first and it is quoted for you.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.
