SQL Set Operations with UNION, INTERSECT, and EXCEPT
Joins combine columns from different tables side by side. But sometimes you need to combine rows from different queries stacked on top of each other. Maybe you want a unified list of all contacts from both a customers table and a suppliers table, or you need to find email addresses that appear in one table but not another. This is where SQL set operations come in.
SQL UNION, INTERSECT, and EXCEPT are set operators that let you combine the result sets of two or more SELECT statements vertically, treating each query's output as a set of rows and applying set theory logic to merge them. This guide explains each operator in detail, walks through the critical difference between UNION and UNION ALL, covers the rules your queries must follow, and provides practical examples with full outputs so you can start using set operations immediately.
What Are Set Operations?
In mathematics, set operations combine groups of items:
- Union produces all items from both sets.
- Intersection produces only items found in both sets.
- Difference produces items in one set but not the other.
SQL applies these same concepts to query result sets. Each SELECT statement produces a set of rows, and the set operator determines how those row sets are combined.
Query A result: Query B result:
┌───────────┐ ┌───────────┐
│ Alice │ │ Carol │
│ Bob │ │ Alice │
│ Carol │ │ Dave │
└───────────┘ └───────────┘
UNION: Alice, Bob, Carol, Dave (all unique)
UNION ALL: Alice, Bob, Carol, Carol, Alice, Dave (everything, duplicates kept)
INTERSECT: Alice, Carol (in both)
EXCEPT: Bob (in A but not B)
The Sample Schema
All examples use these tables:
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
city VARCHAR(50)
);
CREATE TABLE suppliers (
id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
city VARCHAR(50)
);
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
city VARCHAR(50)
);
CREATE TABLE current_products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100),
price DECIMAL(10,2)
);
CREATE TABLE archived_products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100),
price DECIMAL(10,2)
);
-- customers
INSERT INTO customers VALUES (1, 'Alice Martin', 'alice@example.com', 'New York');
INSERT INTO customers VALUES (2, 'Bob Jones', 'bob@example.com', 'Chicago');
INSERT INTO customers VALUES (3, 'Carol Smith', 'carol@example.com', 'Denver');
INSERT INTO customers VALUES (4, 'Dave Wilson', 'dave@example.com', 'New York');
-- suppliers
INSERT INTO suppliers VALUES (1, 'Acme Parts', 'contact@acme.com', 'Chicago');
INSERT INTO suppliers VALUES (2, 'Beta Supply', 'info@betasupply.com','Denver');
INSERT INTO suppliers VALUES (3, 'Carol Smith', 'carol@example.com', 'Denver');
-- employees
INSERT INTO employees VALUES (1, 'Alice Martin', 'alice@corp.com', 'New York');
INSERT INTO employees VALUES (2, 'Eve Turner', 'eve@corp.com', 'Boston');
INSERT INTO employees VALUES (3, 'Frank Lee', 'frank@corp.com', 'Chicago');
-- current_products
INSERT INTO current_products VALUES (101, 'Wireless Mouse', 25.99);
INSERT INTO current_products VALUES (102, 'USB-C Cable', 9.99);
INSERT INTO current_products VALUES (103, 'Mechanical Keyboard', 89.99);
INSERT INTO current_products VALUES (104, 'Desk Lamp', 34.50);
-- archived_products
INSERT INTO archived_products VALUES (102, 'USB-C Cable', 9.99);
INSERT INTO archived_products VALUES (105, 'Wired Mouse', 12.99);
INSERT INTO archived_products VALUES (106, 'VGA Cable', 7.50);
INSERT INTO archived_products VALUES (103, 'Mechanical Keyboard', 79.99);
UNION: Combining Rows from Multiple Queries
The SQL UNION operator takes the results of two SELECT statements and merges them into a single result set, removing duplicate rows in the process.
Basic Syntax
SELECT columns FROM table_a
UNION
SELECT columns FROM table_b;
Example: A Unified Contact List
Suppose you need a single list of all contact names and emails from both customers and suppliers:
SELECT name, email
FROM customers
UNION
SELECT name, email
FROM suppliers;
Output:
| name | |
|---|---|
| Acme Parts | contact@acme.com |
| Alice Martin | alice@example.com |
| Bob Jones | bob@example.com |
| Beta Supply | info@betasupply.com |
| Carol Smith | carol@example.com |
| Dave Wilson | dave@example.com |
Notice that Carol Smith appears in both tables with the same name and email, but she shows up only once in the result. UNION automatically eliminates duplicate rows.
UNION considers two rows as duplicates only when all column values in both rows are identical. If even one column differs, the rows are treated as distinct.
UNION Across Three or More Queries
You can chain multiple UNION operators:
SELECT name, email, 'Customer' AS source FROM customers
UNION
SELECT name, email, 'Supplier' AS source FROM suppliers
UNION
SELECT name, email, 'Employee' AS source FROM employees;
Output:
| name | source | |
|---|---|---|
| Acme Parts | contact@acme.com | Supplier |
| Alice Martin | alice@corp.com | Employee |
| Alice Martin | alice@example.com | Customer |
| Beta Supply | info@betasupply.com | Supplier |
| Bob Jones | bob@example.com | Customer |
| Carol Smith | carol@example.com | Customer |
| Carol Smith | carol@example.com | Supplier |
| Dave Wilson | dave@example.com | Customer |
| Eve Turner | eve@corp.com | Employee |
| Frank Lee | frank@corp.com | Employee |
Notice something important: Alice Martin now appears twice (once as a Customer, once as an Employee) because the source column has a different value, making the full rows distinct. Carol Smith also appears twice for the same reason. The UNION deduplication looks at the entire row, not individual columns.
UNION ALL: Keep Every Row, Including Duplicates
While UNION removes duplicates, UNION ALL keeps every row from every query, even if identical rows appear multiple times.
Syntax
SELECT columns FROM table_a
UNION ALL
SELECT columns FROM table_b;
Example: Compare UNION vs UNION ALL
-- UNION: removes the duplicate Carol Smith row
SELECT name, email FROM customers
UNION
SELECT name, email FROM suppliers;
-- Result: 6 rows
UNION Output:
| name | |
|---|---|
| Alice Martin | alice@example.com |
| Acme Parts | contact@acme.com |
| Beta Supply | info@betasupply.com |
| Bob Jones | bob@example.com |
| Carol Smith | carol@example.com |
| Dave Wilson | dave@example.com |
-- UNION ALL: keeps everything
SELECT name, email FROM customers
UNION ALL
SELECT name, email FROM suppliers;
-- Result: 7 rows
UNION ALL Output:
| name | |
|---|---|
| Alice Martin | alice@example.com |
| Bob Jones | bob@example.com |
| Carol Smith | carol@example.com |
| Dave Wilson | dave@example.com |
| Acme Parts | contact@acme.com |
| Beta Supply | info@betasupply.com |
| Carol Smith | carol@example.com |
Carol Smith appears twice because UNION ALL does not check for or remove duplicates.
When to Use UNION ALL Instead of UNION
UNION ALL should be your default choice unless you specifically need deduplication. Here is why:
| Aspect | UNION | UNION ALL |
|---|---|---|
| Duplicate handling | Removes duplicates | Keeps all rows |
| Performance | Slower (must sort or hash to find duplicates) | Faster (no dedup step) |
| Use when | You need a distinct list | You need all rows, or you know there are no duplicates |
Performance matters. UNION must compare every row from both result sets to identify duplicates. On large datasets, this sorting or hashing step can be expensive. If your data is structured so duplicates cannot exist (for example, each query selects from a different source with non-overlapping data), always use UNION ALL to skip that unnecessary work.
Practical Example: Combining Logs
A common real-world use of UNION ALL is combining event logs from partitioned or archived tables:
SELECT order_id, order_date, total_amount, 'current' AS period
FROM orders_2024
UNION ALL
SELECT order_id, order_date, total_amount, 'archive' AS period
FROM orders_2023
UNION ALL
SELECT order_id, order_date, total_amount, 'archive' AS period
FROM orders_2022
ORDER BY order_date DESC;
Since order IDs are unique across years, duplicates are impossible, making UNION ALL the correct and efficient choice.
INTERSECT: Rows Present in Both Queries
INTERSECT returns only rows that appear in the result sets of both queries. It is the SQL equivalent of a mathematical set intersection.
Syntax
SELECT columns FROM table_a
INTERSECT
SELECT columns FROM table_b;
Example: Contacts Who Are Both Customers and Suppliers
SELECT name, email
FROM customers
INTERSECT
SELECT name, email
FROM suppliers;
Output:
| name | |
|---|---|
| Carol Smith | carol@example.com |
Only Carol Smith has identical name and email values in both tables.
Example: Products That Exist in Both Current and Archived Catalogs
SELECT product_id, product_name
FROM current_products
INTERSECT
SELECT product_id, product_name
FROM archived_products;
Output:
| product_id | product_name |
|---|---|
| 102 | USB-C Cable |
| 103 | Mechanical Keyboard |
Like UNION, INTERSECT compares the entire row across all selected columns. If you included the price column, the Mechanical Keyboard would be excluded because its price differs between the two tables (89.99 vs 79.99).
Including price changes the result:
SELECT product_id, product_name, price
FROM current_products
INTERSECT
SELECT product_id, product_name, price
FROM archived_products;
| product_id | product_name | price |
|---|---|---|
| 102 | USB-C Cable | 9.99 |
Now only the USB-C Cable matches, because its price is 9.99 in both tables.
Example: Cities Where We Have Both Customers and Suppliers
SELECT city FROM customers
INTERSECT
SELECT city FROM suppliers;
| city |
|---|
| Chicago |
| Denver |
Both Chicago and Denver appear in the customers and suppliers tables.
EXCEPT: Rows in One Query but Not the Other
EXCEPT returns rows from the first query that do not appear in the second query. It is the SQL equivalent of set difference (sometimes called set subtraction).
Syntax
SELECT columns FROM table_a
EXCEPT
SELECT columns FROM table_b;
Oracle uses the keyword MINUS instead of EXCEPT. The behavior is identical. If you are working with Oracle, replace EXCEPT with MINUS in the examples below.
Example: Customers Who Are Not Suppliers
SELECT name, email
FROM customers
EXCEPT
SELECT name, email
FROM suppliers;
Output:
| name | |
|---|---|
| Alice Martin | alice@example.com |
| Bob Jones | bob@example.com |
| Dave Wilson | dave@example.com |
Carol Smith is excluded because her name and email match a row in the suppliers table.
Order Matters with EXCEPT
Unlike UNION and INTERSECT, the order of the two queries in EXCEPT changes the result. A minus B is not the same as B minus A.
Suppliers who are not customers:
SELECT name, email
FROM suppliers
EXCEPT
SELECT name, email
FROM customers;
| name | |
|---|---|
| Acme Parts | contact@acme.com |
| Beta Supply | info@betasupply.com |
Completely different result from the previous query because the roles are reversed.
Example: Products Only in the Current Catalog
SELECT product_id, product_name
FROM current_products
EXCEPT
SELECT product_id, product_name
FROM archived_products;
| product_id | product_name |
|---|---|
| 101 | Wireless Mouse |
| 104 | Desk Lamp |
These products exist only in the current catalog and were never archived.
Example: Products Only in the Archive (Discontinued)
SELECT product_id, product_name
FROM archived_products
EXCEPT
SELECT product_id, product_name
FROM current_products;
| product_id | product_name |
|---|---|
| 105 | Wired Mouse |
| 106 | VGA Cable |
These products were archived and no longer appear in the current catalog.
Rules: Column Count and Data Type Matching
All set operations follow strict rules about the structure of the queries being combined. Violating these rules produces an error.
Rule 1: Same Number of Columns
Every SELECT in the set operation must return the same number of columns.
Wrong (different column counts):
SELECT name, email, city
FROM customers
UNION
SELECT name, email
FROM suppliers;
ERROR: each UNION query must have the same number of columns
Correct (add a placeholder or remove the extra column):
-- Option A: Add a placeholder column
SELECT name, email, city
FROM customers
UNION
SELECT name, email, NULL AS city
FROM suppliers;
| name | city | |
|---|---|---|
| Acme Parts | contact@acme.com | null |
| Alice Martin | alice@example.com | New York |
| Beta Supply | info@betasupply.com | null |
| Bob Jones | bob@example.com | Chicago |
| Carol Smith | carol@example.com | null |
| Carol Smith | carol@example.com | Denver |
| Dave Wilson | dave@example.com | New York |
-- Option B: Remove the extra column
SELECT name, email
FROM customers
UNION
SELECT name, email
FROM suppliers;
| name | |
|---|---|
| Acme Parts | contact@acme.com |
| Alice Martin | alice@example.com |
| Beta Supply | info@betasupply.com |
| Bob Jones | bob@example.com |
| Carol Smith | carol@example.com |
| Carol Smith | carol@example.com |
| Dave Wilson | dave@example.com |