UNION in PostgreSQL

Learn how to use PostgreSQL UNION and UNION ALL to combine result sets from multiple SELECT queries, with rules, examples, and performance guidance.

5 min read · Back to overview

Quick Answer

UNION combines the result sets of two or more SELECT statements and removes duplicate rows. UNION ALL does the same but keeps all rows, including duplicates, and is faster because it skips the deduplication step.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

The UNION operator combines the result sets of two or more SELECT queries into a single result set. By default, UNION removes duplicate rows. Use UNION ALL to retain all rows including duplicates — it is faster because no deduplication is performed.

Syntax

SELECT select_list FROM table_a
UNION
SELECT select_list FROM table_b;

To keep duplicates:

SELECT select_list FROM table_a
UNION ALL
SELECT select_list FROM table_b;

To sort the combined result, add ORDER BY after the final query:

SELECT select_list FROM table_a
UNION ALL
SELECT select_list FROM table_b
ORDER BY sort_expression;

Practical Example

Suppose you manage two event registration systems that were merged. Each still has its own table:

CREATE TABLE registrations_2024 (
  attendee_id   SERIAL PRIMARY KEY,
  full_name     VARCHAR NOT NULL,
  email         VARCHAR NOT NULL,
  event_slug    VARCHAR NOT NULL
);

CREATE TABLE registrations_2025 (
  attendee_id   SERIAL PRIMARY KEY,
  full_name     VARCHAR NOT NULL,
  email         VARCHAR NOT NULL,
  event_slug    VARCHAR NOT NULL
);

INSERT INTO registrations_2024 (full_name, email, event_slug) VALUES
  ('Alice Nguyen',   'alice@example',   'summit-2024'),
  ('Bob Okafor',     'bob@example',     'summit-2024'),
  ('Clara Hoffman',  'clara@example',   'workshop-2024');

INSERT INTO registrations_2025 (full_name, email, event_slug) VALUES
  ('Alice Nguyen',   'alice@example',   'summit-2025'),
  ('David Reyes',    'david@example',   'summit-2025'),
  ('Clara Hoffman',  'clara@example',   'workshop-2025');

List every email address across both years, removing duplicates:

SELECT email FROM registrations_2024
UNION
SELECT email FROM registrations_2025
ORDER BY email;

Result:

        email
----------------------
 alice@example
 bob@example
 clara@example
 david@example

Alice and Clara appear in both tables but UNION returns each email once.

List all registrations including duplicates across both years (for counting purposes):

SELECT full_name, email, event_slug FROM registrations_2024
UNION ALL
SELECT full_name, email, event_slug FROM registrations_2025
ORDER BY full_name, event_slug;

Result:

   full_name   |         email          |    event_slug
---------------+------------------------+----------------
 Alice Nguyen  | alice@example      | summit-2024
 Alice Nguyen  | alice@example      | summit-2025
 Bob Okafor    | bob@example        | summit-2024
 Clara Hoffman | clara@example      | workshop-2024
 Clara Hoffman | clara@example      | workshop-2025
 David Reyes   | david@example      | summit-2025

UNION ALL preserves both Alice and Clara’s registrations from each year.

Using UNION to combine aggregates

You can also UNION aggregated queries to produce a combined summary:

SELECT 'Q1 2024' AS period, COUNT(*) AS registrations FROM registrations_2024 WHERE event_slug LIKE '%-2024'
UNION ALL
SELECT 'Q1 2025' AS period, COUNT(*) AS registrations FROM registrations_2025 WHERE event_slug LIKE '%-2025'
ORDER BY period;

UNION vs UNION ALL Performance

OperationDeduplicationRelative SpeedUse when
UNIONYes (implicit DISTINCT)SlowerRows may overlap between queries
UNION ALLNoFasterRows are known distinct or all rows needed

For large result sets, UNION ALL avoids a costly sort-and-hash pass. When in doubt, check with EXPLAIN ANALYZE to see whether the deduplication step is worth the cost.

Testing with Vela

When consolidating tables from separate systems (like the dual-registration example above), use Vela’s database branching to test your UNION queries against a production-data branch before the migration goes live. You can validate deduplication logic, verify column type compatibility, and confirm ORDER BY behavior without any impact on the live database.

Production Tips

  • Prefer UNION ALL over UNION when you know the component queries return non-overlapping rows. The deduplication in UNION requires a sort or hash of the full combined result set.
  • Column names in the output come from the first SELECT. Use aliases in the first query to control the output column names.
  • You cannot place ORDER BY, LIMIT, or FETCH inside individual component queries — only after the final query in the chain. Wrap individual queries in parentheses if you need to LIMIT before combining.
  • For combining data from many tables with the same structure (e.g., partitioned monthly log tables), UNION ALL over a set of similar tables is a common pattern. Consider a partitioned table or a view that UNION ALLs the partitions for cleaner access.
  • When component queries have mismatched data types for corresponding columns, use explicit CAST in the first query to establish the output type: CAST(amount AS NUMERIC(10,2)).

Continue in Set Operations: INTERSECT.

Related in this section: INTERSECT · EXCEPT

Frequently Asked Questions

What is the difference between UNION and UNION ALL in PostgreSQL?
UNION combines result sets and removes duplicate rows by performing an implicit DISTINCT on the combined output. UNION ALL combines result sets and keeps every row, including duplicates. UNION ALL is faster because it skips the sort-and-deduplicate step. Use UNION when duplicates are meaningless in the output; use UNION ALL when the queries are known to return distinct rows or when you want every row preserved.
Does UNION lock the tables it queries?
UNION is a read-only operation. Each SELECT component acquires an ACCESS SHARE lock on the tables it reads, which does not block concurrent reads or writes. The locks are held for the duration of the query.
What are the rules for using UNION in PostgreSQL?
All SELECT statements must return the same number of columns in the same order. Data types of corresponding columns must be compatible (PostgreSQL will attempt implicit casting). ORDER BY, LIMIT, and FETCH can only appear after the final SELECT in the UNION chain, not inside individual component queries. Column names in the output come from the first SELECT.
Can I use ORDER BY with UNION in PostgreSQL?
Yes, but only as a single ORDER BY placed after the final SELECT in the UNION chain. PostgreSQL raises an error if you place ORDER BY inside an individual component query. The ORDER BY applies to the entire combined result and can reference column names from the first SELECT or ordinal positions.
What is the safest way to use UNION in production?
Prefer UNION ALL over UNION when you know the component queries return non-overlapping rows — it avoids an expensive sort-and-deduplicate operation on large result sets. Use explicit CAST for columns where data types differ between queries. Always place ORDER BY and LIMIT after the final query in the chain.