INTERSECT and EXCEPT: Set Operations in Practice
AI generated
SELECT
JOIN
SQL / Set Operators
INTERSECT and EXCEPT: Set Operations in Practice
When set operators are more readable than equivalent JOIN or NOT EXISTS constructs

UNION and UNION ALL belong to the standard toolkit, but the two other set operators in the SQL standard, INTERSECT and EXCEPT, lead a shadow existence in many codebases, even though for certain questions they are the clearest and most maintainable solution available. INTERSECT answers which rows appear in two result sets at the same time, EXCEPT answers which rows appear in the first set but not the second. Both questions can also be solved with JOIN or NOT EXISTS constructs, but the set operator often makes the actual business intent visible at a glance. This article covers when switching is worth it, how syntax differs between databases, and how performance actually compares to the alternatives.

10 min read INTERSECT EXCEPT

1. Basic principle: set comparison instead of row combination

A JOIN combines rows from two tables based on a matching key and returns a combined row from both sources. INTERSECT and EXCEPT work fundamentally differently: both compare two complete result sets with identical column structure against each other and return a set of the same structure, not a combined row but a filtered one. That makes them the natural choice for questions that are genuinely set theoretic in nature, for example which customers ordered both this quarter and last quarter, or which products are held in stock but have never been sold.

Technically both operators behave like UNION: they require the same number of columns with compatible data types in both subqueries, and both remove duplicates from the result by default, similar to UNION without ALL. Some databases now also offer INTERSECT ALL and EXCEPT ALL, which handle duplicates by taking their frequency into account instead of removing them entirely, which can matter for certain inventory comparisons.

2. INTERSECT in practice: customers who ordered in two periods

A classic use case for INTERSECT is finding customers who placed at least one order in each of two different time periods, for example to distinguish repeat buyers from one time buyers. Instead of building a query with a double self join and matching date filters, the INTERSECT version reads almost exactly like the original business question.

The decisive readability advantage shows up once more than two conditions need to be combined. A chain of three or four INTERSECT joins remains extendable by exactly one additional SELECT block for each further condition, while the equivalent JOIN solution requires an additional join and additional null handling for each further condition, which quickly makes the query hard to follow.


-- Customers who ordered in both Q1 and Q2
SELECT customer_id FROM orders
WHERE order_date BETWEEN '2026-01-01' AND '2026-03-31'
INTERSECT
SELECT customer_id FROM orders
WHERE order_date BETWEEN '2026-04-01' AND '2026-06-30';

3. EXCEPT in practice: products with no sales

EXCEPT returns every row from the first query that does not appear in the second query, which makes it excellent for typical difference questions like which products are held in stock but have not sold a single unit in the current year. The semantic closeness to the business question makes the code noticeably more accessible to colleagues without deep SQL expertise than a nested NOT EXISTS construct with a correlated subquery.

It is important to note that EXCEPT, like INTERSECT, operates on the full row, not just a single key column. If only certain columns should drive the comparison, both SELECT lists must be reduced exactly to those columns, otherwise even small differences in accompanying columns like a timestamp will cause a row that is otherwise identical to be incorrectly treated as different.


-- Products in stock with no sales in the current year
SELECT product_id FROM inventory
WHERE quantity_on_hand > 0
EXCEPT
SELECT product_id FROM order_items
WHERE order_date >= '2026-01-01';

4. The equivalent JOIN and NOT EXISTS variants compared

The same INTERSECT query can also be written with an INNER JOIN on the customer id, additionally requiring DISTINCT to remove duplicates from multiple orders within the same period. This version is functionally equivalent, but it requires the reader to mentally reconstruct that a pure existence check is intended, even though the syntax describes a join with all its possible side effects such as duplication in one to many relationships right there in the code.

The EXCEPT query can equivalently be written with a NOT EXISTS subquery that checks, for every row of the outer query, whether a matching row exists in the second set. NOT EXISTS is functionally very close to EXCEPT and in many cases even faster, because the optimizer often executes a correlated subquery as an anti join, but the code becomes noticeably harder to read than a chain of several EXCEPT blocks once multiple conditions are chained together.


-- Equivalent to EXCEPT, functionally identical
SELECT i.product_id
FROM inventory i
WHERE i.quantity_on_hand > 0
  AND NOT EXISTS (
      SELECT 1 FROM order_items oi
      WHERE oi.product_id = i.product_id
        AND oi.order_date >= '2026-01-01'
  );

5. Performance comparison: set operator vs. JOIN vs. NOT EXISTS

In modern optimizer implementations of PostgreSQL, SQL Server, and Oracle, INTERSECT and EXCEPT are frequently mapped internally onto the same execution plan as a semantically equivalent NOT EXISTS or semi join construct, especially when both subqueries are based on indexed columns. In practice, a look at EXPLAIN ANALYZE often shows that runtime barely differs between the three variants on well indexed tables, since the optimizer correctly recognizes the actual intent behind all three formulations.

One relevant difference remains though: INTERSECT and EXCEPT implicitly force duplicate removal across the entire row, which on very wide result sets can mean an additional sort or hash step that a deliberately written NOT EXISTS variant without DISTINCT does not need. For performance critical queries on very large tables, an explicit plan comparison is therefore worthwhile before choosing the set operator purely for readability reasons.

6. Oracle special case: MINUS instead of EXCEPT

Oracle has historically never had an EXCEPT operator, using the keyword MINUS instead with identical semantics: every row from the first query that does not appear in the second query. Anyone who wants to keep code portable between Oracle and other databases must explicitly account for this detail, since an EXCEPT copied over verbatim gets rejected in Oracle with a plain syntax error.

INTERSECT, on the other hand, is available in Oracle under the same name as in the SQL standard and behaves identically to PostgreSQL, SQL Server, and MySQL from version 8.0.31 onward. For database agnostic code, either a central abstraction layer that swaps MINUS and EXCEPT depending on the target database is recommended, or simply avoiding EXCEPT from the start in favor of a NOT EXISTS formulation that works identically across all relevant databases.


-- Oracle version of EXCEPT
SELECT product_id FROM inventory
WHERE quantity_on_hand > 0
MINUS
SELECT product_id FROM order_items
WHERE order_date >= DATE '2026-01-01';

7. MySQL availability and older versions

MySQL only supports INTERSECT and EXCEPT since version 8.0.31, considerably later than most other set operations, which had long been available. In older MySQL versions, which are still running in plenty of production environments, both operations must necessarily be recreated with JOIN or NOT EXISTS constructs, there is no native alternative.

For migration projects that need to support code across older and newer MySQL versions at the same time, a deliberate decision is worth making: either consistently use the compatible NOT EXISTS formulation, or add a version check up front and switch between the two variants depending on the target environment. For new projects that run exclusively on MySQL 8.0.31 or newer, PostgreSQL, SQL Server, or Oracle, there is little reason to avoid the native operators directly.

8. How null values are handled in set operators

An often overlooked difference from a classic equality check with the equals operator concerns null values. While a JOIN on equals never treats two null values as equal, because null equals null in SQL fundamentally evaluates to unknown instead of true, INTERSECT and EXCEPT treat two rows with null in the same column as equivalent for the purpose of set comparison. That matches the behavior of DISTINCT, not the behavior of an equality operator in a WHERE clause.

This difference can lead to surprising results when a team replaces a NOT EXISTS expression with EXCEPT without accounting for this detail: rows with null in the comparison column that would have survived under NOT EXISTS due to SQL's three valued logic can drop out under EXCEPT, because they are considered equivalent to another null row in the second set. An explicit test with real null values in the test data is therefore essential when migrating between the two formulations.

9. Decision guide: when switching is worth it

INTERSECT and EXCEPT are particularly worthwhile when the business question is genuinely phrased in set theoretic terms, when several similar conditions need to be chained together, or when the code is read by people less familiar with complex JOIN constructs. They are less suitable when additional columns from both sources need to appear in the result, since a classic JOIN is fundamentally the right choice for that, because set operators only ever return rows matching the structure of the first subquery.

As a rule of thumb: whenever the phrasing which rows appear in both sets or which rows are missing from the second set sounds natural, a set operator is usually the right choice. As soon as additional information from the second source is needed in the result, for example an order date or a product name from the joined table, there is no way around a classic JOIN.

Operator Meaning Oracle name MySQL since version
INTERSECT Rows present in both sets at once INTERSECT 8.0.31
EXCEPT Rows only in the first set MINUS 8.0.31
INTERSECT ALL like INTERSECT, considers frequency not available not available
EXCEPT ALL like EXCEPT, considers frequency not available not available
Equivalent via JOIN INNER JOIN with DISTINCT identical available since always
Equivalent via NOT EXISTS correlated subquery as anti join identical available since always

Mironsoft

Database optimization, query tuning, and migrations

SQL queries that keep getting slower as the data grows?

We analyze and optimize SQL databases regardless of the system in use, plan safe migrations and schema changes, and teach teams query optimization hands-on.

Query Optimization

Analyze slow queries and speed them up with purpose using indexes and explain plans.

Migration Planning

Execute schema changes and data migrations safely, without downtime.

Team Training

Anchor SQL fundamentals and performance thinking hands-on in the dev team.

10. Summary

INTERSECT and EXCEPT: Key Takeaways

Set comparison instead of joining

INTERSECT and EXCEPT compare two complete result sets and return filtered rows instead of combined rows like a JOIN does.

Oracle uses MINUS

Anyone writing portable code needs to replace EXCEPT with the keyword MINUS on Oracle, INTERSECT stays unchanged.

Performance usually on par

With good indexing, the optimizer often turns set operators, JOINs, and NOT EXISTS constructs into similar execution plans.

Null handling like DISTINCT

Unlike an equality operator, both operators treat two null values as equivalent for the purpose of set comparison.

11. FAQ: INTERSECT and EXCEPT: Key Takeaways

1What is the fundamental difference between INTERSECT and a JOIN?
A JOIN combines columns from two tables into a new row, INTERSECT compares two complete result sets with identical structure and returns only rows present in both sets.
2Does EXCEPT automatically remove duplicates?
Yes, EXCEPT behaves like UNION without ALL and removes duplicates from the result. EXCEPT ALL, where the database supports it, instead takes the frequency of rows into account.
3Why does Oracle not have EXCEPT?
Oracle historically introduced the keyword MINUS before the SQL standard defined EXCEPT, and kept that name for compatibility reasons. The semantics of both keywords are identical.
4Since when does MySQL support INTERSECT and EXCEPT?
Only since MySQL 8.0.31. In older versions both operations must be recreated using JOIN or NOT EXISTS constructs.
5Can I display additional columns from the second query with INTERSECT?
No, the result of a set operator only ever inherits the column structure of the first subquery. For additional columns from the second source, a classic JOIN is the right choice.
6Are INTERSECT and NOT EXISTS always equally fast?
With good indexing, usually yes, since the optimizer often maps both onto the same plan. On very wide result sets, the implicit duplicate removal of INTERSECT can require an additional sort step.
7How are null values handled in INTERSECT and EXCEPT?
Two rows with null in the same column are considered equivalent for the purpose of the set comparison, similar to DISTINCT. That differs from the behavior of an equality operator, where null equals null evaluates to unknown instead of true.
8Do both subqueries in INTERSECT need exactly the same column names?
No, what matters is only the number and compatibility of the data types in the same order. The column names in the result come from the first subquery.
9When is a NOT EXISTS formulation better than EXCEPT?
When portability to Oracle or older MySQL versions matters, or when fine grained control over the comparison condition is needed that goes beyond a plain row comparison.
10Are INTERSECT ALL and EXCEPT ALL available in every database?
No, PostgreSQL supports both variants, Oracle and MySQL do not so far. There the frequency aspect has to be recreated manually with additional counting columns.