LearnFrenzy
Sign in Take a free test

Subqueries and Set Operations · Lesson 18 of 30

SQL UNION, INTERSECT and EXCEPT

5 min read Updated Nov 7, 2026 Saurabh Samir

Joins put tables side by side. Set operations stack query results on top of each other: combine two lists (UNION), keep only what both have (INTERSECT), or what one has and the other doesn't (EXCEPT). They're useful for merging data from similar tables, building reports from several queries and comparing datasets.

In this lesson

  • UNION vs UNION ALL
  • The rules for combining queries
  • INTERSECT and EXCEPT
  • Sorting and labelling combined results

Set operations on query A and query B

  • UNIONA + B, duplicates removed
  • UNION ALLA + B, all rows kept
  • INTERSECTrows in both A and B
  • EXCEPTrows in A but not B

UNION and UNION ALL

-- Every city we know about: customer cities plus a list of new delivery cities
SELECT city FROM customers WHERE city IS NOT NULL
UNION
SELECT 'Hyderabad'
UNION
SELECT 'Pune'
ORDER BY city;

Output

+-----------+
| city      |
+-----------+
| Bengaluru |
| Delhi     |
| Hyderabad |
| Kochi     |
| Mumbai    |
| Pune      |
+-----------+
SELECT city FROM customers WHERE city IS NOT NULL
UNION ALL
SELECT 'Pune'
ORDER BY city;

Output

+-----------+
| city      |
+-----------+
| Bengaluru |
| Delhi     |
| Delhi     |
| Kochi     |
| Mumbai    |
| Pune      |
| Pune      |
| Pune      |
+-----------+

UNION removes duplicates (Pune appears once); UNION ALL keeps every row (Pune three times). Removing duplicates means sorting or hashing all rows, so use UNION ALL whenever duplicates are impossible or wanted – it's faster.

The rules

  • Each query must return the same number of columns, with compatible types.
  • Column names come from the first query.
  • ORDER BY and LIMIT go once, at the very end, and apply to the combined result.

Labelling where rows came from

-- One "activity" list from two different tables
SELECT 'signup' AS event, name AS who, signup_date AS event_date
FROM customers WHERE signup_date >= '2025-06-01'
UNION ALL
SELECT 'hire', name, hire_date
FROM employees WHERE hire_date >= '2024-01-01'
ORDER BY event_date;

Output

+--------+--------------+------------+
| event  | who          | event_date |
+--------+--------------+------------+
| hire   | Tom Mathew   | 2024-01-15 |
| hire   | Imran Shaikh | 2024-08-01 |
| signup | Neha Gupta   | 2025-06-25 |
| signup | Vikram Das   | 2025-07-30 |
+--------+--------------+------------+

A report with a total row

SELECT status, COUNT(*) AS orders FROM orders GROUP BY status
UNION ALL
SELECT 'TOTAL', COUNT(*) FROM orders
ORDER BY status = 'TOTAL', orders DESC, status;

Output

+-----------+--------+
| status    | orders |
+-----------+--------+
| delivered |      7 |
| pending   |      2 |
| shipped   |      2 |
| cancelled |      1 |
| TOTAL     |     12 |
+-----------+--------+

INTERSECT: rows in both

-- Customers who ordered in BOTH Aug–Sep and Oct–Nov (repeat customers across the two periods)
SELECT customer_id FROM orders WHERE order_date >= '2025-08-01' AND order_date < '2025-10-01'
INTERSECT
SELECT customer_id FROM orders WHERE order_date >= '2025-10-01' AND order_date < '2025-12-01'
ORDER BY customer_id;

Output

+-------------+
| customer_id |
+-------------+
|           1 |
|           2 |
|           3 |
|           4 |
+-------------+

EXCEPT: rows in the first but not the second

-- Customers who ordered in Aug–Sep but NOT in Oct–Nov (customers we may be losing)
SELECT customer_id FROM orders WHERE order_date >= '2025-08-01' AND order_date < '2025-10-01'
EXCEPT
SELECT customer_id FROM orders WHERE order_date >= '2025-10-01' AND order_date < '2025-12-01';

Output

+-------------+
| customer_id |
+-------------+
|           5 |
+-------------+

Version and dialect notes

  • MySQL supports INTERSECT and EXCEPT from 8.0.31. On older versions use IN/EXISTS for INTERSECT and NOT EXISTS for EXCEPT.
  • Oracle calls EXCEPT MINUS.
  • Like UNION, INTERSECT and EXCEPT remove duplicates; NULLs are treated as equal to each other here (unlike in WHERE).
-- The same EXCEPT written with NOT EXISTS (works on every version)
SELECT DISTINCT a.customer_id
FROM orders a
WHERE a.order_date >= '2025-08-01' AND a.order_date < '2025-10-01'
  AND NOT EXISTS (SELECT 1 FROM orders b
                  WHERE b.customer_id = a.customer_id
                    AND b.order_date >= '2025-10-01' AND b.order_date < '2025-12-01');

Output

+-------------+
| customer_id |
+-------------+
|           5 |
+-------------+

Practice

Try it yourself

  1. Make one list of all people – customers and employees – with a column saying which they are, sorted by name (first 6).

    Show solution
    SELECT name, 'customer' AS type FROM customers
    UNION ALL
    SELECT name, 'employee' FROM employees
    ORDER BY name
    LIMIT 6;

    Output

    +--------------+----------+
    | name         | type     |
    +--------------+----------+
    | Anita Desai  | employee |
    | Arjun Singh  | customer |
    | Asha Verma   | customer |
    | Dev Patel    | employee |
    | Farhan Ali   | employee |
    | Imran Shaikh | employee |
    +--------------+----------+

Quick check

UNION vs UNION ALL?

UNION removes duplicate rows; UNION ALL keeps all rows and is faster.

Where do the result's column names come from?

The first query.

How do you write EXCEPT in Oracle?

MINUS.

Summary

  • UNION stacks results without duplicates; UNION ALL keeps everything and is faster.
  • Queries need the same number of compatible columns; ORDER BY goes at the end.
  • INTERSECT keeps common rows, EXCEPT keeps rows only in the first query (MySQL 8.0.31+).

Interview practice: SQL tricky output questions.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular