sqlpostgresqlunionmysql

UNION vs UNION ALL in SQL: Combining Query Results

How UNION and UNION ALL differ, the column compatibility rules you must follow, and how to sort the combined result.

13 min čitanjaReferencesql · postgresql · union · mysql · clickhouse
Ovaj članak trenutno je na ruskom — engleski prijevod je u izradi.

В SQL есть два популярных способа объединять данные из разных запросов:

UNION

и:

UNION ALL

На первый взгляд они почти одинаковые. Оба позволяют взять результат одного SELECT и добавить под него результат другого SELECT.

То есть строки не соединяются рядом, как в JOIN, а складываются вертикально — одна выборка под другой.

Например, у нас есть пользователи из одной таблицы и клиенты из другой. Мы хотим получить один общий список email. В такой ситуации как раз может пригодиться UNION или UNION ALL.

Но между ними есть важная разница:

  • UNION ALL оставляет все строки, включая дубликаты;
  • UNION убирает повторяющиеся строки.

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

Разберёмся спокойно и на примерах.

Что делает UNION простыми словами

Представим два запроса.

Первый возвращает страны пользователей:

SELECT country
FROM users;

Результат:

country
--------
Vietnam
Germany
France

Второй возвращает страны сотрудников:

SELECT country
FROM employees;

Результат:

country
--------
Germany
Spain
Vietnam

Если мы хотим получить один общий список стран, можно объединить результаты:

SELECT country
FROM users

UNION

SELECT country
FROM employees;

Результат будет таким:

country
--------
Vietnam
Germany
France
Spain

Обратите внимание: Vietnam и Germany были в обеих выборках, но в итоговом результате остались по одному разу.

Так работает UNION: он объединяет строки и убирает дубликаты.

Что делает UNION ALL

Теперь возьмём тот же пример, но используем UNION ALL.

SELECT country
FROM users

UNION ALL

SELECT country
FROM employees;

Результат будет другим:

country
--------
Vietnam
Germany
France
Germany
Spain
Vietnam

Здесь все строки остались на месте.

Если Germany была в первой выборке и во второй, она появится два раза. Если Vietnam была в первой выборке и во второй, она тоже появится два раза.

UNION ALL ничего не удаляет. Он просто берёт строки из первого запроса и дописывает под них строки из второго.

Можно запомнить так:

UNION ALL — сложить всё как есть. UNION — сложить и убрать полные дубликаты.

UNION — это не JOIN

Новички иногда путают UNION и JOIN, потому что оба оператора как будто «объединяют таблицы».

Но делают они совершенно разные вещи.

JOIN соединяет таблицы горизонтально: добавляет колонки рядом.

Например:

SELECT
  users.email,
  orders.amount
FROM users
JOIN orders ON orders.user_id = users.id;

Результат может быть таким:

email           | amount
----------------+--------
anna@mail.com   | 1500
bob@mail.com    | 2300

Здесь данные пользователя и заказа стоят в одной строке.

А UNION соединяет результаты вертикально: добавляет строки вниз.

Например:

SELECT email
FROM users

UNION ALL

SELECT email
FROM subscribers;

Результат:

email
----------------
anna@mail.com
bob@mail.com
kate@mail.com
tom@mail.com

Можно представить так:

JOIN      — приклеить колонки сбоку
UNION     — положить строки друг под друга

Это очень важное различие.

Простой пример с двумя списками email

Допустим, у нас есть таблица users:

id | email
---+----------------
1  | anna@mail.com
2  | bob@mail.com
3  | kate@mail.com

И таблица subscribers:

id | email
---+----------------
1  | bob@mail.com
2  | tom@mail.com
3  | kate@mail.com

Мы хотим получить общий список email.

Если нам нужны все строки как есть, используем UNION ALL:

SELECT email
FROM users

UNION ALL

SELECT email
FROM subscribers;

Результат:

email
--------------
anna@mail.com
bob@mail.com
kate@mail.com
bob@mail.com
tom@mail.com
kate@mail.com

bob@mail.com и kate@mail.com повторяются, потому что они есть в обеих таблицах.

Если нам нужен уникальный список email, используем UNION:

SELECT email
FROM users

UNION

SELECT email
FROM subscribers;

Результат:

email
--------------
anna@mail.com
bob@mail.com
kate@mail.com
tom@mail.com

Теперь каждый email встречается только один раз.

Главное отличие UNION и UNION ALL

Разница короткая, но очень важная.

UNION ALL

оставляет все строки.

UNION

удаляет дубликаты.

Например:

SELECT 'SQL' AS course
UNION ALL
SELECT 'SQL' AS course;

Результат:

course
------
SQL
SQL

А теперь:

SELECT 'SQL' AS course
UNION
SELECT 'SQL' AS course;

Результат:

course
------
SQL

В первом случае строки остались обе. Во втором случае одинаковые строки схлопнулись в одну.

Почему UNION может быть медленнее

UNION ALL работает проще.

База берёт результат первого запроса, потом результат второго запроса и просто складывает их вместе.

А вот UNION должен сделать дополнительную работу: найти и удалить дубликаты.

Для этого базе нужно сравнить строки между собой. На больших объёмах это может означать:

  • сортировку результата;
  • построение хеш-таблицы;
  • дополнительную память;
  • временные файлы на диске;
  • более долгий план выполнения.

На маленькой таблице разницы можно не заметить.

Но если вы объединяете миллионы строк, лишний UNION может стать серьёзной проблемой.

Поэтому в реальных проектах часто используют правило:

Если дубликаты не мешают или вы точно знаете, что их нет, используйте UNION ALL.

А UNION используйте только тогда, когда вам действительно нужно убрать повторы.

Когда лучше использовать UNION ALL

UNION ALL обычно стоит брать по умолчанию, если нет явной причины удалять дубликаты.

Хорошие случаи для UNION ALL:

  • объединить заказы за разные месяцы;
  • сложить архивную и текущую таблицу;
  • собрать события из разных источников;
  • объединить данные из разных шардов;
  • подготовить общую витрину, где строки не пересекаются;
  • сохранить все факты без потери повторов.

Например, есть таблица заказов за январь:

SELECT id, user_id, amount, created_at
FROM orders_2026_01

И таблица заказов за февраль:

SELECT id, user_id, amount, created_at
FROM orders_2026_02

Если один и тот же заказ не может одновременно лежать в январской и февральской таблице, нет смысла использовать UNION.

Лучше так:

SELECT id, user_id, amount, created_at
FROM orders_2026_01

UNION ALL

SELECT id, user_id, amount, created_at
FROM orders_2026_02;

Такой запрос просто сложит строки без лишней проверки на дубликаты.

Когда нужен UNION

UNION нужен, когда дубликаты действительно возможны и их нужно убрать.

Например, мы собираем общий список email из нескольких источников:

SELECT email
FROM users

UNION

SELECT email
FROM subscribers

UNION

SELECT email
FROM leads;

Если один и тот же email есть в нескольких таблицах, в итоговом списке он останется один раз.

Это полезно для задач вроде:

  • получить уникальный список клиентов;
  • собрать уникальные страны;
  • объединить справочники;
  • убрать повторы после нескольких источников;
  • подготовить список для рассылки без дублей.

Но важно понимать: UNION удаляет только полностью одинаковые строки.

Что считается дубликатом в UNION

UNION сравнивает всю строку целиком, а не только первую колонку.

Например:

SELECT 'anna@mail.com' AS email, 'user' AS source

UNION

SELECT 'anna@mail.com' AS email, 'lead' AS source;

Результат:

email          | source
---------------+-------
anna@mail.com  | lead
anna@mail.com  | user

Почему email один и тот же, но строки не схлопнулись?

Потому что вторая колонка отличается:

user
lead

Для UNION это разные строки.

А вот такой запрос:

SELECT 'anna@mail.com' AS email, 'user' AS source

UNION

SELECT 'anna@mail.com' AS email, 'user' AS source;

вернёт одну строку:

email          | source
---------------+-------
anna@mail.com  | user

Потому что совпали все колонки.

Главное правило:

UNION удаляет дубликаты по всей строке целиком.

Как UNION работает с NULL

В обычных сравнениях SQL NULL ведёт себя непривычно: NULL = NULL не даёт TRUE.

Но при удалении дублей в UNION две строки с NULL в одинаковых местах считаются одинаковыми.

Например:

SELECT NULL AS country

UNION

SELECT NULL AS country;

Результат:

country
-------
NULL

Останется одна строка.

А с UNION ALL будет две:

SELECT NULL AS country

UNION ALL

SELECT NULL AS country;

Результат:

country
-------
NULL
NULL

Для новичка это может выглядеть странно, но в контексте дедупликации UNION считает такие строки дубликатами.

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

У всех SELECT внутри UNION должно быть одинаковое количество колонок.

Такой запрос корректный:

SELECT id, email
FROM users

UNION ALL

SELECT id, email
FROM subscribers;

В обоих запросах по две колонки.

А такой запрос упадёт:

SELECT id, email
FROM users

UNION ALL

SELECT id, user_id, amount
FROM orders;

Почему?

Потому что в первом SELECT две колонки:

id, email

А во втором три:

id, user_id, amount

База не понимает, как складывать такие строки друг под друга.

Это как пытаться объединить две таблицы такого вида:

id | email

и:

id | user_id | amount

Структура строк должна совпадать.

Порядок колонок важен

В UNION важны не только количество колонок, но и их порядок.

Например:

SELECT id, email
FROM users

UNION ALL

SELECT user_id, status
FROM orders;

Технически здесь две колонки и две колонки. Запрос может выполниться, если типы совместимы.

Но по смыслу результат будет странный:

id | email
---+--------
1  | anna@mail.com
2  | bob@mail.com
1  | paid
2  | failed

Вторая колонка сначала содержит email, а потом статус заказа. Формально это может быть текст, но логически это разные сущности.

Поэтому при UNION важно не просто сделать одинаковое количество колонок, а убедиться, что колонки совпадают по смыслу.

Хороший результат должен быть похож на одну общую таблицу.

Типы колонок должны быть совместимы

Колонки на одинаковых позициях должны иметь совместимые типы.

Например:

SELECT id, email
FROM users

UNION ALL

SELECT id, status
FROM orders;

Если email и status оба текстовые, база может объединить их.

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

Например:

SELECT id, email::text AS value
FROM users

UNION ALL

SELECT id, amount::text AS value
FROM orders;

Здесь мы явно приводим email и amount к тексту, чтобы получить общий формат.

Но нужно быть аккуратным: если приходится приводить типы, спросите себя, действительно ли эти данные должны лежать в одной колонке.

Иногда правильнее добавить дополнительные колонки и сделать результат понятнее.

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

Ещё одна важная особенность: названия колонок в итоговом результате берутся из первого SELECT.

Например:

SELECT id, email
FROM users

UNION ALL

SELECT user_id, status
FROM orders;

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

id | email

Даже если во втором запросе они назывались user_id и status.

Поэтому первый SELECT лучше оформлять аккуратно и давать колонкам понятные алиасы.

Например:

SELECT
  id,
  email AS value,
  'user' AS source
FROM users

UNION ALL

SELECT
  id,
  status AS value,
  'order' AS source
FROM orders;

Результат:

id | value          | source
---+----------------+--------
1  | anna@mail.com  | user
2  | paid           | order

Теперь видно, откуда пришла строка и что лежит в колонке value.

Зачем добавлять колонку source

В реальных задачах при объединении данных часто полезно добавить техническую колонку с источником.

Например:

SELECT
  id,
  email,
  'users' AS source
FROM users

UNION ALL

SELECT
  id,
  email,
  'subscribers' AS source
FROM subscribers;

Результат:

id | email          | source
---+----------------+------------
1  | anna@mail.com  | users
2  | bob@mail.com   | users
1  | bob@mail.com   | subscribers
2  | tom@mail.com   | subscribers

Колонка source помогает понять, из какой таблицы пришла строка.

Это особенно полезно при:

  • отладке отчётов;
  • объединении нескольких источников;
  • миграциях;
  • сравнении старой и новой системы;
  • построении витрин данных.

Без такой колонки можно быстро потерять контекст.

Как объединить разные сущности в одну таблицу

Иногда нужно собрать общий список событий, хотя данные лежат в разных таблицах.

Например, есть регистрации пользователей и заказы.

Таблица users:

id | email          | created_at
---+----------------+---------------------
1  | anna@mail.com  | 2026-06-01 10:00:00

Таблица orders:

id | user_id | amount | created_at
---+---------+--------+---------------------
10 | 1       | 1500   | 2026-06-02 12:00:00

Хотим получить одну ленту событий:

event_time           | event_type | description
---------------------+------------+---------------------
2026-06-01 10:00:00  | signup     | anna@mail.com
2026-06-02 12:00:00  | order      | 1500

Запрос:

SELECT
  created_at AS event_time,
  'signup' AS event_type,
  email AS description
FROM users

UNION ALL

SELECT
  created_at AS event_time,
  'order' AS event_type,
  amount::text AS description
FROM orders

ORDER BY event_time;

Здесь мы специально приводим amount к тексту:

amount::text

потому что в колонке description в первой ветке лежит email, а во второй — сумма заказа.

Так мы строим единую ленту из разных типов событий.

Как добавить NULL для отсутствующих колонок

Бывает, что у одной сущности есть колонка, а у другой её нет.

Например, у заказов есть amount, а у пользователей — нет.

Но для UNION количество колонок должно совпадать. Тогда можно добавить NULL в той ветке, где значения нет.

SELECT
  id,
  email,
  NULL::numeric AS amount,
  'user' AS source
FROM users

UNION ALL

SELECT
  id,
  NULL::text AS email,
  amount,
  'order' AS source
FROM orders;

Результат:

id | email          | amount | source
---+----------------+--------+--------
1  | anna@mail.com  | NULL   | user
10 | NULL           | 1500   | order

Зачем писать NULL::numeric и NULL::text?

Чтобы явно подсказать PostgreSQL тип колонки.

Обычный NULL сам по себе не имеет конкретного типа. Когда запрос сложный, явное приведение помогает избежать неоднозначности и делает структуру результата понятнее.

ORDER BY при UNION

Если нужно отсортировать итоговый результат, ORDER BY пишется один раз — в самом конце.

Например:

SELECT id, name, salary
FROM employees
WHERE dept = 'eng'

UNION ALL

SELECT id, name, salary
FROM employees
WHERE dept = 'sales'

ORDER BY salary DESC;

Здесь сортируется весь объединённый результат: и сотрудники из eng, и сотрудники из sales.

Важно: финальный ORDER BY относится ко всему результату после UNION.

Не к первой ветке. Не ко второй ветке. А ко всему набору строк.

LIMIT при UNION

LIMIT тоже обычно ставится в конце, если нужно ограничить весь итоговый результат.

Например:

SELECT id, name, salary
FROM employees
WHERE dept = 'eng'

UNION ALL

SELECT id, name, salary
FROM employees
WHERE dept = 'sales'

ORDER BY salary DESC
LIMIT 10;

Такой запрос вернёт топ-10 сотрудников по зарплате среди двух отделов вместе.

То есть сначала объединяем сотрудников из eng и sales, потом сортируем по зарплате, потом берём первые 10 строк.

Как ограничить только одну ветку

Иногда нужно применить ORDER BY и LIMIT не ко всему результату, а только к одному SELECT.

Например, взять топ-5 оплаченных заказов и добавить к ним все возвраты.

Тогда ветку нужно обернуть в подзапрос:

SELECT *
FROM (
  SELECT id, amount, status
  FROM orders
  WHERE status = 'paid'
  ORDER BY amount DESC
  LIMIT 5
) AS top_paid

UNION ALL

SELECT id, amount, status
FROM orders
WHERE status = 'refunded';

Здесь ORDER BY amount DESC LIMIT 5 применяется только к первой части.

Без подзапроса можно случайно получить совсем другой смысл или ошибку синтаксиса в зависимости от СУБД.

Практическое правило:

Если сортировка или лимит нужны для отдельной ветки UNION, заверните эту ветку в подзапрос.

ORDER BY по номеру колонки

В итоговом ORDER BY можно ссылаться на колонку по имени или по номеру.

Например:

SELECT id, name, salary
FROM employees
WHERE dept = 'eng'

UNION ALL

SELECT id, name, salary
FROM employees
WHERE dept = 'sales'

ORDER BY 3 DESC;

ORDER BY 3 означает: сортировать по третьей колонке.

В нашем случае третья колонка — это salary.

Но для новичков лучше писать явно:

ORDER BY salary DESC

Так запрос проще читать.

Номера колонок иногда удобны в быстрых аналитических запросах, но в учебном и продуктовом коде понятные имена обычно лучше.

Скобки и порядок выполнения

UNION и UNION ALL можно комбинировать.

Например:

SELECT email FROM users

UNION ALL

SELECT email FROM subscribers

UNION

SELECT email FROM leads;

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

Чтобы явно показать порядок, можно использовать скобки.

Например:

(
  SELECT email FROM users
  UNION ALL
  SELECT email FROM subscribers
)

UNION

SELECT email FROM leads;

Смысл такой:

  1. сначала объединяем users и subscribers через UNION ALL;
  2. потом объединяем результат с leads через UNION, удаляя дубликаты.

Если логика сложная, лучше не писать всё одной длинной цепочкой. Используйте скобки или CTE, чтобы следующий человек понял ваш замысел.

UNION ALL и дальнейшая дедупликация

Иногда удобнее сначала собрать всё через UNION ALL, а потом отдельно решить, как именно удалять дубликаты.

Например:

WITH all_emails AS (
  SELECT email, 'users' AS source FROM users
  UNION ALL
  SELECT email, 'subscribers' AS source FROM subscribers
  UNION ALL
  SELECT email, 'leads' AS source FROM leads
)
SELECT DISTINCT email
FROM all_emails;

Такой вариант читается явно:

  1. all_emails — собрали все email из всех источников;
  2. потом выбрали уникальные email.

Это особенно удобно, если нужна не просто дедупликация всей строки, а более тонкая логика.

Например, оставить по одному email, но при этом выбрать приоритетный источник.

Частая ошибка: использовать UNION вместо UNION ALL «на всякий случай»

Многие новички ставят UNION, потому что он кажется более «аккуратным»: вдруг будут дубли?

Но это не всегда хорошо.

Например, вы объединяете события из двух независимых источников:

SELECT user_id, event_name, created_at
FROM web_events

UNION

SELECT user_id, event_name, created_at
FROM mobile_events;

Если одинаковое событие пришло из web и mobile, UNION может схлопнуть строки, хотя в аналитике это могут быть два разных факта.

В результате вы потеряете данные.

Если вам нужны все события, правильнее:

SELECT user_id, event_name, created_at
FROM web_events

UNION ALL

SELECT user_id, event_name, created_at
FROM mobile_events;

А если нужно различать источники, добавьте колонку:

SELECT user_id, event_name, created_at, 'web' AS source
FROM web_events

UNION ALL

SELECT user_id, event_name, created_at, 'mobile' AS source
FROM mobile_events;

Теперь даже одинаковые события из разных источников не потеряются, потому что источник явно сохранён.

Частая ошибка: думать, что UNION удаляет дубли по одной колонке

Допустим, есть запрос:

SELECT email, 'users' AS source
FROM users

UNION

SELECT email, 'leads' AS source
FROM leads;

Если один и тот же email есть в обеих таблицах, он всё равно появится два раза:

email          | source
---------------+-------
anna@mail.com  | users
anna@mail.com  | leads

Почему?

Потому что строки отличаются по колонке source.

Если вам нужен уникальный список только по email, нужно выбирать только email:

SELECT email
FROM users

UNION

SELECT email
FROM leads;

Или собрать всё через UNION ALL, а потом сделать отдельную дедупликацию по email.

Например:

WITH all_emails AS (
  SELECT email, 'users' AS source FROM users
  UNION ALL
  SELECT email, 'leads' AS source FROM leads
)
SELECT DISTINCT email
FROM all_emails;

Главная мысль:

UNION удаляет дубли по всем выбранным колонкам, а не по одной выбранной вами колонке в голове.

Пример: архивная и текущая таблица

Очень частый рабочий сценарий — есть текущая таблица и архив.

Например:

orders_current
orders_archive

У них одинаковая структура:

id | user_id | amount | status | created_at

Чтобы получить все заказы, можно написать:

SELECT id, user_id, amount, status, created_at
FROM orders_current

UNION ALL

SELECT id, user_id, amount, status, created_at
FROM orders_archive;

Почему здесь обычно UNION ALL?

Потому что архив и текущая таблица, как правило, не должны пересекаться. Если данные разложены корректно, один и тот же заказ не лежит одновременно и там, и там.

Если поставить UNION, база будет тратить силы на проверку дублей, которые по бизнес-логике невозможны.

Пример: отчёт по нескольким источникам

Допустим, у нас есть заказы из сайта и мобильного приложения.

SELECT
  user_id,
  amount,
  created_at,
  'web' AS source
FROM web_orders

UNION ALL

SELECT
  user_id,
  amount,
  created_at,
  'mobile' AS source
FROM mobile_orders;

Так мы получаем общий поток заказов.

Потом его можно агрегировать:

WITH all_orders AS (
  SELECT user_id, amount, created_at, 'web' AS source
  FROM web_orders

  UNION ALL

  SELECT user_id, amount, created_at, 'mobile' AS source
  FROM mobile_orders
)
SELECT
  source,
  COUNT(*) AS orders_count,
  SUM(amount) AS revenue
FROM all_orders
GROUP BY source;

Результат:

source | orders_count | revenue
-------+--------------+---------
web    | 1500         | 2300000
mobile | 2100         | 3400000

Здесь UNION ALL особенно важен: мы считаем факты заказов, а не уникальные строки.

Как проверить, нужны ли дубли

Если вы сомневаетесь, можно сначала посмотреть, есть ли пересечения.

Например, есть две таблицы email:

SELECT email FROM users
UNION ALL
SELECT email FROM leads;

Чтобы найти повторяющиеся email после объединения:

WITH all_emails AS (
  SELECT email FROM users
  UNION ALL
  SELECT email FROM leads
)
SELECT
  email,
  COUNT(*) AS cnt
FROM all_emails
GROUP BY email
HAVING COUNT(*) > 1;

Так вы увидите, какие значения встречаются больше одного раза.

После этого уже можно осознанно решить:

  • оставить дубли, потому что это разные факты;
  • убрать дубли, потому что нужен уникальный список;
  • добавить колонку source;
  • дедуплицировать по специальным правилам.

UNION, UNION ALL и DISTINCT

По смыслу:

UNION

похож на:

UNION ALL + DISTINCT

То есть такой запрос:

SELECT email FROM users

UNION

SELECT email FROM leads;

примерно похож по идее на:

SELECT DISTINCT email
FROM (
  SELECT email FROM users
  UNION ALL
  SELECT email FROM leads
) AS all_emails;

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

Например, можно добавить source, дату загрузки, приоритет и уже потом выбрать нужные строки.

Отличия в PostgreSQL

В PostgreSQL UNION и UNION ALL работают строго и предсказуемо.

PostgreSQL проверяет:

  • одинаковое количество колонок;
  • совместимость типов;
  • корректность итогового ORDER BY.

При UNION PostgreSQL должен удалить дубликаты. В плане запроса это может выглядеть как сортировка или хеширование.

Чтобы посмотреть, что реально делает база, можно использовать:

EXPLAIN
SELECT country FROM users
UNION
SELECT country FROM employees;

Или с фактическим выполнением:

EXPLAIN ANALYZE
SELECT country FROM users
UNION
SELECT country FROM employees;

Если таблицы большие, полезно сравнить план для UNION и UNION ALL.

Часто вы увидите, что UNION ALL проще и дешевле.

Отличия в MySQL

В MySQL идея такая же:

SELECT email FROM users
UNION
SELECT email FROM leads;

удаляет дубликаты.

А:

SELECT email FROM users
UNION ALL
SELECT email FROM leads;

оставляет все строки.

Но MySQL может быть мягче в неявных приведениях типов. Например, если в одной ветке число, а в другой текст, он может попытаться привести значения сам.

Иногда это удобно, но иногда приводит к неожиданным результатам. Поэтому для понятного запроса лучше явно выравнивать типы через CAST.

Ещё один практический момент: если в MySQL нужно применить ORDER BY или LIMIT к отдельной ветке, используйте подзапрос.

Например:

SELECT *
FROM (
  SELECT id, amount
  FROM orders
  WHERE status = 'paid'
  ORDER BY amount DESC
  LIMIT 5
) AS top_paid

UNION ALL

SELECT id, amount
FROM orders
WHERE status = 'refunded';

Так смысл будет явным.

Отличия в ClickHouse

В ClickHouse UNION ALL встречается особенно часто, потому что ClickHouse часто используют для аналитики и больших потоков данных.

Пример:

SELECT user_id, event_name, created_at
FROM web_events

UNION ALL

SELECT user_id, event_name, created_at
FROM mobile_events;

Для аналитических событий обычно важно не потерять строки, поэтому UNION ALL часто оказывается правильным выбором.

Если нужно убрать дубликаты, в ClickHouse обычно явно используют DISTINCT или агрегацию после объединения.

Например:

SELECT DISTINCT email
FROM (
  SELECT email FROM users
  UNION ALL
  SELECT email FROM leads
);

Общее правило остаётся тем же:

если нужны все строки — UNION ALL; если нужен уникальный результат — добавляйте дедупликацию осознанно.

Практические шаблоны

Объединить две таблицы без удаления дублей

SELECT email
FROM users

UNION ALL

SELECT email
FROM subscribers;

Получить уникальный список email

SELECT email
FROM users

UNION

SELECT email
FROM subscribers;

Добавить источник строки

SELECT id, email, 'users' AS source
FROM users

UNION ALL

SELECT id, email, 'subscribers' AS source
FROM subscribers;

Объединить текущие и архивные заказы

SELECT id, user_id, amount, status, created_at
FROM orders_current

UNION ALL

SELECT id, user_id, amount, status, created_at
FROM orders_archive;

Сделать общую ленту событий

SELECT created_at AS event_time, 'signup' AS event_type, email AS description
FROM users

UNION ALL

SELECT created_at AS event_time, 'order' AS event_type, amount::text AS description
FROM orders

ORDER BY event_time;

Отсортировать общий результат

SELECT id, name, salary
FROM employees
WHERE dept = 'eng'

UNION ALL

SELECT id, name, salary
FROM employees
WHERE dept = 'sales'

ORDER BY salary DESC;

Ограничить весь объединённый результат

SELECT id, name, salary
FROM employees
WHERE dept = 'eng'

UNION ALL

SELECT id, name, salary
FROM employees
WHERE dept = 'sales'

ORDER BY salary DESC
LIMIT 10;

Ограничить только одну ветку

SELECT *
FROM (
  SELECT id, amount
  FROM orders
  WHERE status = 'paid'
  ORDER BY amount DESC
  LIMIT 5
) AS top_paid

UNION ALL

SELECT id, amount
FROM orders
WHERE status = 'refunded';

Что важно запомнить

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

То есть строки второго SELECT добавляются под строки первого SELECT.

Главная разница:

UNION ALL — оставляет все строки
UNION     — удаляет дубликаты

UNION ALL обычно быстрее, потому что базе не нужно искать повторы.

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

У всех SELECT внутри объединения должно быть:

  • одинаковое количество колонок;
  • совместимые типы колонок;
  • одинаковый смысл колонок на одинаковых позициях.

Имена итоговых колонок берутся из первого SELECT.

Если нужно понять, откуда пришла строка, добавляйте колонку-источник:

SELECT email, 'users' AS source FROM users
UNION ALL
SELECT email, 'leads' AS source FROM leads;

Если нужен общий порядок, ORDER BY пишется в самом конце.

Короткий вывод

UNION и UNION ALL нужны, когда вы хотите сложить результаты нескольких SELECT друг под друга.

В большинстве рабочих задач стоит начинать с UNION ALL.

Он проще, быстрее и не удаляет строки без вашего явного решения.

SELECT email FROM users
UNION ALL
SELECT email FROM leads;

А UNION используйте тогда, когда вам действительно нужен уникальный результат:

SELECT email FROM users
UNION
SELECT email FROM leads;

Главная мысль:

UNION ALL сохраняет факты. UNION делает уникальный список.

Если вы объединяете события, заказы, логи или архивы — чаще всего нужен UNION ALL.

Если вы собираете список уникальных email, стран, кодов или справочных значений — может подойти UNION.

Хороший SQL-разработчик выбирает между ними не наугад, а по смыслу данных: нужно ли сохранять все строки или действительно нужно убрать дубликаты.

Vježbaj na stvarnim zadacima

Rješavaj zadatke u SQL treneru uz trenutno ocjenjivanje i savjete.

Otvori trener