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 обязательны два условия:
- Одинаковое количество колонок в обоих запросах
- Совместимые типы данных в соответствующих позициях
Имена колонок берутся из первого запроса:
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-выражения, которые позволяют именовать промежуточные результаты и делать сложные запросы читаемыми.