Skip to content
Common mistakes / SQL: Why UNION Drops Rows From a Total
Common mistake

SQL: Why UNION Drops Rows From a Total

UNION removes duplicate rows from its result, so two different records that happen to share every selected value become one. Stack results with UNION ALL, which keeps every row.

The symptom

You check Garden Shop's revenue by adding up the lines of shipped orders and the lines of every other order, stacked with UNION. The shop has 48 order lines worth 1,129.00 in all. Your query counts 26 lines and 637.50, and nothing errors.

Why it happens

UNION stacks two results and then removes duplicate rows, the way DISTINCT does. A duplicate means every selected column is equal, and here the only column is the amount. Five different lines are worth 24.00 each, so UNION keeps one of them. It removes repeats inside each half too, not only between the halves.

Rows that look like duplicates in the result aren't duplicates in the shop: they're different lines with the same total. That's why a UNION around amounts, counts, or dates breaks totals.

The fix

Use UNION ALL, which stacks the results and keeps every row:

Wrong: UNION removes equal amounts
with line_amounts as (
  select oi.quantity * oi.unit_price as amount
  from order_items as oi
  join orders as o on o.order_id = oi.order_id
  where o.status = 'shipped'
  union
  select oi.quantity * oi.unit_price
  from order_items as oi
  join orders as o on o.order_id = oi.order_id
  where o.status <> 'shipped'
)
select count(*) as lines, sum(amount) as revenue
from line_amounts
Right: UNION ALL keeps every line
with line_amounts as (
  select oi.quantity * oi.unit_price as amount
  from order_items as oi
  join orders as o on o.order_id = oi.order_id
  where o.status = 'shipped'
  union all
  select oi.quantity * oi.unit_price
  from order_items as oi
  join orders as o on o.order_id = oi.order_id
  where o.status <> 'shipped'
)
select count(*) as lines, sum(amount) as revenue
from line_amounts

That's 48 lines and 1,129.00 again. Choose UNION only when removing repeats is the point, such as a list of every state that has a customer or a supplier.

Keeping the key also works

Selecting the row's id makes every row distinct, so UNION has nothing to remove:

With the line's id, UNION keeps all 48
with line_amounts as (
  select oi.order_item_id, oi.quantity * oi.unit_price as amount
  from order_items as oi
  join orders as o on o.order_id = oi.order_id
  where o.status = 'shipped'
  union
  select oi.order_item_id, oi.quantity * oi.unit_price
  from order_items as oi
  join orders as o on o.order_id = oi.order_id
  where o.status <> 'shipped'
)
select count(*) as lines, sum(amount) as revenue
from line_amounts

It still spends work comparing every row to find repeats that can't exist. UNION ALL says what you mean and skips that step.

Dialect note

Every engine treats UNION this way; it's the SQL standard. The reverse surprise, duplicate rows you didn't expect from UNION ALL, has its own page: why UNION ALL keeps duplicate rows.

Learn more