UNION vs UNION ALL in SQL: Duplicates, Speed, Pitfalls

· 8 MIN

TL;DR: UNION stacks two result sets and then removes duplicate rows, like a DISTINCT over the whole output. UNION ALL stacks them and keeps every row, so it does less work and is usually faster. Use UNION ALL by default and switch to UNION only when you actually want duplicates gone.

The difference between UNION and UNION ALL in SQL is one step: deduplication. Both operators append the rows of a second SELECT to the rows of the first, and both need the same number of columns with compatible types. UNION then compares every output row and keeps one copy of each distinct row, which costs a sort or hash over the full result. UNION ALL returns everything as is. The PostgreSQL manual puts it plainly: "UNION ALL is usually significantly quicker than UNION; use ALL when you can" (PostgreSQL SELECT reference). The rest of this page shows what each one returns on real rows, what the dedupe looks like in a query plan, and the reporting case where UNION quietly deletes revenue.

UNION vs UNION ALL at a glance

CriterionUNIONUNION ALL
Duplicate rowsRemoved (whole result, both inputs)Kept
Extra workSort or hash over every output rowNone, rows are appended
Typical speedSlower, grows with result sizeFaster
Memory / temp spaceNeeds space for the dedupe stepStreams rows
Row countLess than or equal to the sum of inputsExactly the sum of inputs
NULLsTwo all-NULL rows count as duplicatesBoth kept
Safe for SUM / COUNT afterwardsOnly if duplicates are truly bogusYes
Explicit spellingUNION DISTINCT (PostgreSQL, MySQL)UNION ALL

Behaviour checked against the PostgreSQL 18, SQL Server and SQLite 3.46 documentation and test runs, November 2026.

What is the difference between UNION and UNION ALL?

UNION returns the set of distinct rows that appear in either query; UNION ALL returns every row from both queries, duplicates included. The PostgreSQL docs on combining queries say UNION "eliminates duplicate rows from its result, in the same way as DISTINCT, unless UNION ALL is used." Microsoft's T-SQL UNION reference says the same: ALL "incorporates all rows into the results, including duplicates. If not specified, duplicate rows are removed."

Here is a small payments example. Ana paid $49 twice on the same day through Stripe (two real charges, say two seats), and once through PayPal.

-- stripe_payments: ('ana', 49, '2026-10-01'), ('ana', 49, '2026-10-01'), ('ben', 99, '2026-10-02')
-- paypal_payments: ('ana', 49, '2026-10-01'), ('cy', 19, '2026-10-03')

SELECT customer, amount, paid_on FROM stripe_payments
UNION
SELECT customer, amount, paid_on FROM paypal_payments
ORDER BY customer;
-- 3 rows: ana 49, ben 99, cy 19

SELECT customer, amount, paid_on FROM stripe_payments
UNION ALL
SELECT customer, amount, paid_on FROM paypal_payments
ORDER BY customer;
-- 5 rows: ana 49 (x3), ben 99, cy 19
UNION collapses Ana's three identical $49 rows into one, UNION ALL keeps all five rows
FIG 01 — Same inputs: UNION returns 3 rows, UNION ALL returns 5.

Does UNION remove duplicates within one query?

Yes. UNION deduplicates the combined output, so duplicates inside a single input disappear too, not only rows that appear in both inputs. In the example above, Ana's two identical Stripe rows collapse even before the PayPal row is considered. You can see it with a query against itself:

SELECT customer FROM stripe_payments
UNION
SELECT customer FROM stripe_payments;
-- 2 rows: ana, ben  (stripe_payments alone has 3 rows)

This surprises people who expect UNION to mean "rows from A plus rows from B that are not already in A". That is closer to what UNION ALL plus a NOT EXISTS filter does. UNION is a set operation over the whole result.

Which is faster, UNION or UNION ALL?

UNION ALL is faster, because UNION has to find duplicates and UNION ALL does not. Finding duplicates means sorting the combined rows or building a hash table of them, and the cost grows with the number of rows and their width. The two plans differ by exactly that step.

In PostgreSQL, run EXPLAIN on both. The UNION ALL plan is an Append node over the two scans. The UNION plan puts a HashAggregate (or Sort plus Unique) on top of that Append, and on large results the sort can spill to disk. SQLite shows the same split in plain words:

EXPLAIN QUERY PLAN
SELECT x FROM a UNION SELECT x FROM b;
-- COMPOUND QUERY
--   LEFT-MOST SUBQUERY: SCAN a
--   UNION USING TEMP B-TREE: SCAN b

EXPLAIN QUERY PLAN
SELECT x FROM a UNION ALL SELECT x FROM b;
-- COMPOUND QUERY
--   LEFT-MOST SUBQUERY: SCAN a
--   UNION ALL: SCAN b

The "TEMP B-TREE" is the dedupe structure. UNION ALL has none.

UNION adds a dedupe step after appending rows, UNION ALL returns appended rows directly
FIG 02 — UNION ALL stops at Append; the dedupe step is the whole cost difference.

How much slower UNION is depends on your data, so measure it on yours with EXPLAIN ANALYZE instead of trusting a generic benchmark. Two rules hold everywhere: the gap is small on a few hundred rows and grows with result size, and UNION can never be faster than UNION ALL on the same inputs, since it does the same work plus a step.

When should I use UNION ALL?

Use UNION ALL whenever duplicates are impossible or meaningful. That covers most real queries:

Use UNION when you want a distinct list: every email address that appears in either the customers or the newsletter table, or the set of countries you sell in across two regions.

The reporting trap: UNION silently drops real rows

UNION's dedupe compares whole rows, and nothing warns you when it removes one. In the payments example, total revenue is $265 (49 + 49 + 99 + 49 + 19). Sum the UNION instead and you get $167:

SELECT SUM(amount) FROM (
  SELECT customer, amount, paid_on FROM stripe_payments
  UNION
  SELECT customer, amount, paid_on FROM paypal_payments
) t;
-- 167  (wrong: two real $49 payments were removed)

SELECT SUM(amount) FROM (
  SELECT customer, amount, paid_on FROM stripe_payments
  UNION ALL
  SELECT customer, amount, paid_on FROM paypal_payments
) t;
-- 265  (correct)
Summing a UNION reports $167 because two of Ana's $49 payments were removed; UNION ALL reports $265
FIG 03 — The $98 gap is two legitimate payments that UNION treated as duplicates.

The bug hides because the rows only look identical: you selected too few columns to tell them apart. Two defenses:

  1. Default to UNION ALL in any query that feeds a metric.
  2. If you need UNION, include a key that makes real rows distinct (payment_id, a source literal), so only true duplicates collapse.

If you want to find duplicates rather than drop them, group and count instead; see our guide to finding duplicates with a SQL query.

Rules both operators share

Both operators have the same requirements, in every major database:

-- top 5 per source, then combined (parentheses keep LIMIT on each input)
(SELECT customer, amount FROM stripe_payments ORDER BY amount DESC LIMIT 5)
UNION ALL
(SELECT customer, amount FROM paypal_payments ORDER BY amount DESC LIMIT 5)
ORDER BY amount DESC;

A UNION is also not a JOIN: UNION stacks rows vertically from queries with the same shape, a JOIN puts columns side by side from related tables. Our page on selecting from multiple tables without a JOIN covers when stacking is the right tool.

VIDEO · SQL UNION vs UNION ALL: Understand the Difference with 2 Examples (Ryan & Matt Data Science)

Choose UNION if / choose UNION ALL if

Choose UNION if you need a distinct list across sources, and the selected columns fully identify what "the same row" means for your question.

Choose UNION ALL if the inputs cannot overlap, duplicates are real events, or the result feeds a SUM, COUNT or dashboard metric. When in doubt, pick UNION ALL and dedupe explicitly with DISTINCT or GROUP BY where you mean it, which also makes the intent visible to the next reader.

Where this shows up in reporting

Combining payment processors, regional databases or yearly archive tables is one of the most common reasons teams write a UNION, and one of the easiest places for a wrong total to reach a dashboard. If you'd rather ask "total revenue across Stripe and PayPal last month" in plain English and check the generated SQL before trusting the number, AI for Database shows the query behind every answer and keeps the result on a self-refreshing dashboard. Try AI for Database free at aifordatabase.com. For writing the queries yourself, our SQL cheat sheet and cumulative sum guide are good next steps.

Frequently Asked Questions

Is UNION the same as UNION DISTINCT?

Yes. In PostgreSQL and MySQL, UNION DISTINCT is an explicit spelling of the default behaviour. PostgreSQL's docs call DISTINCT a "noise word" here. SQL Server accepts only UNION and UNION ALL.

Does UNION treat NULLs as equal?

Yes, for deduplication. Two rows that match column by column, with NULL in the same positions, count as duplicates and only one survives. SELECT NULL UNION SELECT NULL returns one row; with UNION ALL it returns two.

Can I use ORDER BY in each part of a UNION?

Not directly. A single ORDER BY after the last query sorts the whole result. To order or limit one input, wrap that SELECT in parentheses, as in the top-5-per-source example above.

Do the column names have to match?

No. Only the count and types have to line up. The result takes its column names from the first SELECT, so the names in later queries are ignored.

Is UNION ALL always faster than UNION?

On the same inputs, UNION ALL is never slower, because UNION does the same appending plus a dedupe step. The size of the gap depends on how many rows the union produces; check it with EXPLAIN ANALYZE.

Does UNION remove duplicates from only the second query?

No. It removes duplicates across the entire combined result, including duplicates that exist inside the first query alone.

Ready to try AI for Database?

Query your database in plain English. No SQL required. Start free today.