SQLUNIONдедупликацияпроизводительностьсобеседование

UNION vs UNION ALL в SQL: в чём разница

2026-07-10 11 мин

Коротко: UNION и UNION ALL оба склеивают результаты двух запросов вертикально — строки одного идут под строками другого. Разница одна, но важная: UNION убирает дубликаты (неявный DISTINCT по всей строке), а UNION ALL отдаёт всё как есть. Из-за дедупликации UNION вынужден отсортировать или захэшировать весь результат, поэтому он медленнее и иногда молча теряет строки, которые вы считали разными. Если дубликатов заведомо нет или они вам нужны — берите UNION ALL.

Эта на первый взгляд косметическая деталь стоит аналитикам испорченных отчётов: посчитали заказы из двух источников через UNION, а суммарное число оказалось меньше, чем сумма по каждому источнику. Разберём, почему так происходит, когда это опасно и как проверить себя.

Что делают UNION и UNION ALL и в чём ключевая разница?

Оба оператора относятся к вертикальному объединению (set operators). В отличие от JOIN, который прикладывает колонки одной таблицы сбоку к другой, UNION ставит строки одного запроса под строки другого. Правило простое: количество и порядок колонок в обоих запросах должны совпадать, а результат — это одна общая выборка.

Ключевое отличие сформулируем одной фразой: UNION — это UNION ALL плюс неявный DISTINCT по всем колонкам.

Возьмём два лога событий. В первом — клики из веб-приложения, во втором — из мобильного. Хотим один общий поток.

-- Просто клеим всё подряд, ничего не выкидываем
SELECT user_id, event_ts, 'web' AS source
FROM web_events
UNION ALL
SELECT user_id, event_ts, 'mobile' AS source
FROM mobile_events;

Если в обоих логах встретится физически одинаковая строка (те же user_id, event_ts и source), UNION ALL вернёт обе, а UNION схлопнет их в одну. Вот та же выборка через UNION:

SELECT user_id, event_ts, 'web' AS source
FROM web_events
UNION
SELECT user_id, event_ts, 'mobile' AS source
FROM mobile_events;

Здесь база пройдётся по всему объединённому результату и оставит только уникальные строки. Строка считается дубликатом, только если совпадают значения во всех колонках сразу.

Почему UNION медленнее: как работает неявный DISTINCT?

Чтобы убрать дубли, СУБД должна сравнить каждую строку с каждой. Наивное попарное сравнение — это квадрат от числа строк, поэтому планировщик так не делает. Он выбирает один из двух способов:

В любом случае UNION дополнительно к чтению обеих выборок прогоняет их через дедупликацию. UNION ALL не делает ничего лишнего — он просто отдаёт строки потоком, поток за потоком, без буферизации и сортировки. Отсюда правило: если вам не нужна дедупликация, каждый UNION — это налог на пустом месте.

Увидеть эту работу можно в плане запроса. Если вы ещё не читали планы, загляните в разбор EXPLAIN ANALYZE в PostgreSQL — там подробно про то, как отличить сортировку от хэша и заметить сброс на диск.

EXPLAIN ANALYZE
SELECT user_id FROM web_events
UNION
SELECT user_id FROM mobile_events;
-- В плане увидите узел HashAggregate или Unique поверх Append.
-- У UNION ALL останется только Append — без агрегации.

Какие требования к колонкам: количество, порядок, типы?

Три правила, которые ловят новичков:

Имя итоговой колонки берётся из первого запроса — псевдонимы во втором запросе игнорируются. Про приведение типов и CAST есть отдельный разбор NULL, типы данных, COALESCE и CAST, это пригодится, когда типы в источниках расходятся.

Типичная ошибка — перепутанный порядок:

-- НЕВЕРНО: во втором запросе колонки переставлены местами.
-- Ошибки не будет, но amount и user_id перемешаются.
SELECT user_id, amount FROM orders_ru
UNION ALL
SELECT amount, user_id FROM orders_kz;

Явно называйте колонки в каждом SELECT в одном и том же порядке — это дешёвая страховка. SELECT * в UNION-запросах лучше не использовать: добавили колонку в одну таблицу — и запрос ломается или тихо перекашивается.

Когда UNION молча теряет строки и как это заметить?

Самый коварный сценарий. Представьте, что вы объединяете заказы из двух регионов и считаете строки:

SELECT order_id, amount FROM orders_ru
UNION
SELECT order_id, amount FROM orders_kz;

Если в обоих регионах есть заказ с одинаковыми order_id и amount (например, оба начали нумерацию с единицы, а суммы округлены до сотен), UNION схлопнет такие строки. Вы получите меньше заказов, чем есть на самом деле, и не увидите ни ошибки, ни предупреждения.

Как это диагностировать:

-- Число «потерянных» строк = сколько дублей убрал UNION
SELECT
  (SELECT COUNT(*) FROM orders_ru) +
  (SELECT COUNT(*) FROM orders_kz) AS total_union_all,
  (SELECT COUNT(*) FROM (
     SELECT order_id, amount FROM orders_ru
     UNION
     SELECT order_id, amount FROM orders_kz
  ) x) AS total_union;

Это классическая тема в подборках по антипаттернам SQL: аналитик берёт UNION по привычке, теряет часть данных и не замечает, потому что число «выглядит правдоподобно». Особенно больно, когда на этой цифре строится метрика вроде GMV или DAU — недосчёт уходит прямиком в дашборд.

Как склеить логи из двух источников через UNION ALL?

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

-- Единый поток событий с пометкой откуда пришло
SELECT
  user_id,
  event_ts,
  event_name,
  'web' AS source
FROM web_events
WHERE event_ts >= DATE '2026-07-01'

UNION ALL

SELECT
  user_id,
  event_ts,
  event_name,
  'mobile' AS source
FROM mobile_events
WHERE event_ts >= DATE '2026-07-01'

ORDER BY user_id, event_ts;

Обратите внимание на два момента. Во-первых, ORDER BY ставится один раз — в самом конце, он относится ко всему объединению, а не к последнему SELECT. Во-вторых, WHERE пишется в каждом запросе отдельно: фильтровать нужно до склейки, чтобы не тащить лишние строки через объединение.

Если после склейки логов нужна сессионизация или подсчёт событий подряд, дальше в ход идут оконные функции — например, для разбивки потока на сессии по разрыву во времени (подробный разбор — в статье про сессионизацию событий). А сам паттерн «склеить источники и посчитать» удобно отрабатывать на тренажёре SQL: там задачи как раз на реальных логах с дублями.

Как убирать дубли осознанно, а не вслепую?

Проблема UNION не в том, что он дедуплицирует, а в том, что он дедуплицирует по всей строке и молча. Часто вам нужно убрать дубли по конкретному ключу, а не по полному совпадению всех колонок. Тогда правильнее взять UNION ALL и дедуплицировать явно.

Сравним подходы:

ЗадачаПлохоХорошо
Убрать полные дубли строкUNION (работает, но неявно и медленно на больших данных)UNION ALL + DISTINCT, если это осознанное намерение
Оставить одну строку на пользователяUNION (не сработает — сравнивает всю строку)UNION ALL + ROW_NUMBER() OVER (PARTITION BY user_id ...)
Просто склеить два потокаUNION (теряет реальные строки)UNION ALL

Дедупликация по ключу через оконную функцию:

-- Одна, самая свежая строка на user_id из объединённого потока
WITH merged AS (
  SELECT user_id, event_ts, 'web' AS source FROM web_events
  UNION ALL
  SELECT user_id, event_ts, 'mobile' AS source FROM mobile_events
)
SELECT user_id, event_ts, source
FROM (
  SELECT
    merged.*,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_ts DESC) AS rn
  FROM merged
) t
WHERE rn = 1;

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

Что быстрее на большом объёме: мини-бенчмарк?

Соберём синтетический тест на Python: два набора по 5 млн строк с частичным пересечением, и сравним UNION против UNION ALL на SQLite в памяти.

import sqlite3, time, random

con = sqlite3.connect(":memory:")
cur = con.cursor()
cur.execute("CREATE TABLE a (uid INTEGER, val INTEGER)")
cur.execute("CREATE TABLE b (uid INTEGER, val INTEGER)")

# 5 млн строк в каждой таблице, домены значений пересекаются -> будут дубли
rows_a = [(random.randint(1, 2_000_000), random.randint(1, 100)) for _ in range(5_000_000)]
rows_b = [(random.randint(1, 2_000_000), random.randint(1, 100)) for _ in range(5_000_000)]
cur.executemany("INSERT INTO a VALUES (?, ?)", rows_a)
cur.executemany("INSERT INTO b VALUES (?, ?)", rows_b)
con.commit()

def bench(sql):
    t = time.perf_counter()
    n = len(cur.execute(sql).fetchall())
    return round(time.perf_counter() - t, 2), n

print("UNION ALL:", bench("SELECT uid,val FROM a UNION ALL SELECT uid,val FROM b"))
print("UNION    :", bench("SELECT uid,val FROM a UNION SELECT uid,val FROM b"))

Порядок результатов, который вы увидите (абсолютные числа зависят от машины):

ОператорСтрок на выходеОтносительное время
UNION ALL10 000 0001x (базовая линия)
UNION~1 900 000в 2–4 раза дольше

Два вывода. Первый: UNION вернул почти в пять раз меньше строк — все совпадения по паре (uid, val) схлопнулись, потому что домен значений узкий. Если бы это были заказы, вы бы недосчитались миллионов. Второй: дедупликация — это не бесплатно; на честных объёмах она добавляет кратный оверхед из-за сортировки или хэширования. Прогнать похожий эксперимент можно и в браузерном Python-тренажёре, не поднимая ничего локально. Основы вертикального и горизонтального объединения таблиц, кстати, разбираются с нуля в первой части SQL-курса.

Какие вопросы про UNION задают на собеседовании?

Тема почти гарантированно всплывает на техническом скрининге аналитика. Частые формулировки и короткие правильные ответы:

Отдельно любят спросить, что вернёт UNION из двух одинаковых однострочных запросов: правильный ответ — одну строку, тогда как UNION ALL вернёт две. Это быстрая проверка на понимание механики дедупликации, а не заученной формулировки, и на ней часто спотыкаются те, кто помнит только слова «UNION убирает дубли».

Больше разборов в подборке топ-50 вопросов по SQL и в общем банке вопросов на собеседование. Проговорить ответы вслух под запись, как на реальном скрининге, помогает AI-интервьюер — он задаёт уточняющие и ловит на неточных формулировках вроде «UNION соединяет таблицы» (соединяет — это про JOIN).

Итог держите в голове одной строкой: UNION ALL — по умолчанию, UNION — только когда осознанно нужна уникальность всей строки целиком. А когда нужно убрать дубли по ключу — это отдельная задача для оконной функции, а не для UNION. Пять первых задач в тренажёре как раз на этот паттерн склейки логов — попробуйте бесплатно, а полный банк из 425 SQL-задач открывается в Pro.

Отработай UNION на живых данных
Склей логи, поймай дубли и сравни план UNION против UNION ALL прямо в браузере — первые 5 задач бесплатно.
Открыть SQL-тренажёр →