UNION и UNION ALL

UNION и UNION ALL

Подзапросы позволяют вкладывать запросы друг в друга. Но иногда нужна другая операция: не объединить таблицы по ключам, а «склеить» результаты двух запросов в один список строк. Для этого существуют операторы UNION и UNION ALL — вертикальное объединение наборов данных.

Идея: склеить два результата

JOIN — горизонтальное объединение: строки расширяются новыми колонками из другой таблицы. UNION — вертикальное: результаты двух запросов объединяются друг под другом в один набор строк.

Синтаксис:

SELECT col1, col2 FROM table_a
UNION
SELECT col1, col2 FROM table_b;

Результат — все строки из первого запроса, затем все строки из второго. UNION по умолчанию убирает дубликаты — как DISTINCT.

Требования к структуре

Для UNION обязательны два условия:

  1. Одинаковое количество колонок в обоих запросах
  2. Совместимые типы данных в соответствующих позициях

Имена колонок берутся из первого запроса:

SELECT name AS person, city FROM customers
UNION
SELECT name, city FROM employees;
-- Колонки называются: person, city (по первому запросу)

Если типы данных несовместимы, PostgreSQL попробует автоматически привести (cast). Если не получится — ошибка.

UNION: убираем дубликаты

-- Все города, где есть клиенты или сотрудники (без повторений)
SELECT city FROM customers
UNION
SELECT city FROM employees;

Если Москва есть и в customers, и в employees — она появится в результате один раз. UNION выполняет сортировку и дедупликацию, что требует дополнительного времени.

Проверь себя: если у 5 клиентов и 3 сотрудников одинаковый город, сколько строк вернёт UNION?

UNION ALL: сохраняем все строки

UNION ALL объединяет без удаления дубликатов. Это быстрее — нет сортировки и дедупликации:

SELECT city FROM customers
UNION ALL
SELECT city FROM employees;
-- Возвращает все строки из обеих таблиц, включая повторения

Правило выбора: если вы знаете, что дубликатов нет или они не важны — всегда используйте UNION ALL. Он быстрее. UNION без ALL — только когда дедупликация действительно нужна.

Практический пример: единый лог событий

Типичная задача — собрать события из нескольких таблиц в единую временную шкалу:

SELECT 'order'    AS event_type,
       o.id       AS event_id,
       c.name     AS actor,
       o.amount   AS value,
       o.created_at AS event_time
FROM   orders    o
JOIN   customers c ON o.customer_id = c.id

UNION ALL

SELECT 'review',
       r.id,
       c.name,
       r.rating,
       r.created_at
FROM   reviews   r
JOIN   customers c ON r.customer_id = c.id

ORDER BY event_time DESC
LIMIT 50;

ORDER BY и LIMIT в конце применяются ко всему объединённому результату. UNION ALL сохраняет все события — дубликатов в разных типах событий нет по природе данных.

ORDER BY в UNION-запросах

ORDER BY можно поставить только один раз, в самом конце, после всех UNION:

SELECT name FROM customers
UNION ALL
SELECT name FROM employees
ORDER BY name;    -- сортирует весь объединённый результат

Нельзя добавить ORDER BY в середину:

-- Ошибка
SELECT name FROM customers ORDER BY name
UNION ALL
SELECT name FROM employees;
-- ERROR: syntax error at or near "UNION"

Если нужна сортировка внутри части — используйте подзапрос:

SELECT name FROM (SELECT name FROM customers ORDER BY name LIMIT 5) top_customers
UNION ALL
SELECT name FROM employees;

Несколько UNION подряд

Можно объединять больше двух запросов:

SELECT id, 'customer' AS type, name FROM customers
UNION ALL
SELECT id, 'employee', name FROM employees
UNION ALL
SELECT id, 'partner',  name FROM partners
ORDER BY name;

Все три набора данных объединяются в один список. PostgreSQL выполняет UNION ALL последовательно слева направо.

INTERSECT и EXCEPT

Связанные операторы, которые реже встречаются, но полезно знать:

INTERSECT — пересечение: строки, которые есть в обоих результатах:

-- Города, где есть и клиенты, и сотрудники
SELECT city FROM customers
INTERSECT
SELECT city FROM employees;

EXCEPT (или MINUS в Oracle) — разность: строки из первого запроса, которых нет во втором:

-- Города клиентов, где нет сотрудников
SELECT city FROM customers
EXCEPT
SELECT city FROM employees;

Оба оператора по умолчанию убирают дубликаты (как UNION). Есть INTERSECT ALL и EXCEPT ALL для сохранения повторений.

Названия колонок в UNION: важная деталь

Имена колонок в результате UNION берутся из первого запроса. Это важно при использовании результата UNION в подзапросе или CTE — нужно знать, как называются колонки.

SELECT id AS user_id, name AS display_name FROM customers
UNION ALL
SELECT id,            name               FROM employees;
-- Колонки результата: user_id, display_name (по первому запросу)

Чтобы избежать путаницы, хорошая практика — давать осмысленные псевдонимы в первом запросе и писать комментарий, что соответствие колонок — по позиции, а не имени.

UNION и производительность

UNION (без ALL) выполняет сортировку для дедупликации. На больших данных это заметно. Если нет явной необходимости в дедупликации — всегда UNION ALL.

Ещё важный момент: ORDER BY в конце всего UNION-запроса сортирует весь объединённый результат. При больших данных из нескольких таблиц это дополнительные ресурсы. Используйте LIMIT вместе с ORDER BY, чтобы не сортировать весь набор.

Реальный сценарий: сводный отчёт по нескольким источникам

Продолжая тему единого лога: в реальных системах часто нужно свести данные из разных таблиц в «плоский» список для отчёта или API. UNION ALL — стандартный инструмент для этого.

Пример: финансовый отчёт, объединяющий приходы (заказы) и расходы (зарплата):

SELECT 'доход'      AS operation_type,
       o.id         AS reference_id,
       o.created_at AS operation_date,
       o.amount     AS amount
FROM   orders o
WHERE  o.status = 'completed'

UNION ALL

SELECT 'расход',
       e.id,
       DATE_TRUNC('month', NOW()) AS operation_date,
       -e.salary   AS amount      -- отрицательное число = расход
FROM   employees e
WHERE  e.status = 'active'

ORDER BY operation_date DESC, amount DESC;

Два принципиально разных источника данных собираются в единую таблицу с одинаковой структурой. Отрицательные значения для расходов — стандартная практика в бухгалтерских системах. ORDER BY в конце сортирует весь список хронологически.

Типичные ошибки

1. Разное количество колонок:

SELECT id, name, city FROM customers
UNION
SELECT id, name FROM employees;
-- ERROR: each UNION query must have the same number of columns

2. Несовместимые типы:

SELECT id, amount FROM orders      -- amount: numeric
UNION
SELECT id, name   FROM employees;  -- name: text
-- ERROR: UNION types numeric and text cannot be matched

3. ORDER BY внутри UNION:

SELECT name FROM customers ORDER BY name   -- нельзя внутри UNION
UNION ALL
SELECT name FROM employees;

Когда использовать UNION, а когда JOIN

UNION и JOIN — разные операции с разными задачами. Спутать их — распространённая ошибка для новичков:

  • JOIN — горизонтальное объединение: добавляет колонки из другой таблицы к строкам. Использовать, когда нужно обогатить строку данными из другой таблицы.
  • UNION — вертикальное объединение: добавляет строки из другого набора данных. Использовать, когда нужно объединить несколько наборов строк в один список.

Быстрая проверка: «нужны ли мне новые колонки (JOIN) или новые строки (UNION)?».

-- JOIN: добавить имя клиента к каждому заказу (новые колонки)
SELECT o.id, c.name, o.amount
FROM orders o JOIN customers c ON o.customer_id = c.id;

-- UNION: получить единый список всех людей (клиенты + сотрудники, новые строки)
SELECT name FROM customers
UNION ALL
SELECT name FROM employees;

Краткий итог

  • UNION объединяет результаты двух запросов в один, убирая дубликаты
  • UNION ALL объединяет без дедупликации — быстрее; предпочтителен когда дубликаты не важны
  • Требования: одинаковое число колонок, совместимые типы данных
  • ORDER BY и LIMIT — только в конце всего выражения
  • INTERSECT — пересечение; EXCEPT — разность наборов

Что дальше

Вы освоили UNION. Финальный урок модуля — CTE (Common Table Expressions): WITH-выражения, которые позволяют именовать промежуточные результаты и делать сложные запросы читаемыми.

Попробуйте интерактивную версию

Практические задачи, квизы и AI-наставник — бесплатный старт без карты

Перейти к практике