codejump

AcademySQL

DISTINCT, UNION and set operations

What deduplication actually costs, the difference between UNION and UNION ALL, and why DISTINCT in a query is often a join that went wrong.

3 min readUpdated

3 exercises

Waiting at the end of this page.

UNION deduplicates, UNION ALL does not

SQL
SELECT email FROM customers
UNION
SELECT email FROM leads;

That returns each email once, across both lists. To get there the engine must compare every row with every other — a sort or a hash over the whole result — and that work happens whether or not any duplicates exist.

SQL
SELECT email FROM customers
UNION ALL
SELECT email FROM leads;

This one concatenates and stops. `UNION ALL` is the default worth having: use it unless you specifically need deduplication, and when you do need it, know you are paying for a sort.

Both forms require the two sides to have the same number of columns, in compatible types and in the same order — the column names come from the first SELECT, and the second one's names are ignored entirely.

INTERSECT and EXCEPT

SQL
-- In both lists
SELECT email FROM customers
INTERSECT
SELECT email FROM leads;

-- In the first, not the second
SELECT email FROM customers
EXCEPT
SELECT email FROM leads;

EXCEPT is MINUS in Oracle. Both deduplicate by default, like UNION.

These read better than the equivalent NOT EXISTS for simple whole-row comparisons — and unlike NOT IN, they handle NULL sanely, treating two NULLs as the same value for the purpose of matching.

You wantUse
Both lists, duplicates keptUNION ALL
Both lists, each row onceUNION
Only rows in bothINTERSECT
Rows in the first onlyEXCEPT
Rows of one table filtered by anothera JOIN or EXISTS, not a set operation

DISTINCT is usually a symptom

SQL
SELECT DISTINCT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id;

This works, and it is almost always the wrong fix. The duplicates are not in customers — the join created them, one row per order. DISTINCT then sorts the whole result to remove work that should not have been done:

SQL
SELECT c.name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

Same answer, no duplicates to remove, and the engine can stop at the first matching order per customer.

The rule of thumb: when you reach for DISTINCT, ask where the duplicates came from. If a join produced them, the question was "does a match exist" — and that is EXISTS.

DISTINCT applies to the whole row

SQL
SELECT DISTINCT country, city FROM customers;

That is distinct pairs, not distinct countries with some city. There is no way to apply it to one column of several — the syntax that looks like it does, DISTINCT(country), city, is just DISTINCT with a redundant pair of parentheses, and it still deduplicates both columns. When you want one row per country with a chosen city, that is GROUP BY country with an aggregate, or a window function ranking the cities.

PostgreSQL has an extension for exactly this:

SQL
SELECT DISTINCT ON (country) country, city
FROM customers
ORDER BY country, population DESC;

One row per country — the first one in the given order, so the ORDER BY is doing the choosing and is not optional. Convenient, and PostgreSQL-only; the portable version is ROW_NUMBER() in a subquery.

Now practice it

2 challenges · 1 quiz

Reading this page is the cheap half. These are the exercises that make you use it.