SQL: операции над наборами¶
Иногда нужно объединить результаты нескольких запросов в одну таблицу, найти пересечение двух выборок или исключить строки одного набора из другого. Для этого в SQL есть четыре оператора: UNION ALL, UNION, INTERSECT и EXCEPT.
Эти операции — рабочие лошадки ETL: сбор данных из нескольких источников, дедупликация, сверка витрин. Все примеры на схеме etl_kitchen_db.
Правила для всех операций¶
Перед тем как начать — три правила, общие для UNION, INTERSECT и EXCEPT:
- Одинаковое количество колонок в обоих запросах
- Совместимые типы данных (PostgreSQL попробует привести автоматически, но лучше явно)
- Имена колонок берутся из первого запроса
-- Ошибка: разное количество колонок
SELECT name, email FROM customers
UNION ALL
SELECT name FROM employees; -- ← не хватает второй колонки
-- Правильно: добавь заглушку
SELECT name, email FROM customers
UNION ALL
SELECT name, NULL AS email FROM employees;
UNION ALL: склеить всё¶
UNION ALL — самый простой и быстрый оператор. Просто ставит результаты один под другой. Дубликаты сохраняются.
Единый поток событий из разных таблиц¶
-- Собираем лог активности: заказы + события + отзывы
SELECT
c.name,
'order' AS event_type,
o.order_date AS event_date,
o.total_amount::text AS details
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
UNION ALL
SELECT
c.name,
'review' AS event_type,
r.created_at::date AS event_date,
r.rating::text || ' звёзд' AS details
FROM reviews r
JOIN customers c ON r.customer_id = c.customer_id
UNION ALL
SELECT
c.name,
e.event_type,
e.event_date::date,
e.page_url AS details
FROM events e
JOIN customers c ON e.customer_id = c.customer_id
ORDER BY name, event_date DESC
LIMIT 10;
| name | event_type | event_date | details |
|---|---|---|---|
| Алексей Смирнов | order | 2025-01-15 | 45990.00 |
| Алексей Смирнов | review | 2024-12-20 | 5 звёзд |
| Алексей Смирнов | page_view | 2024-12-19 | /products/1 |
UNION ALL — по умолчанию
Всегда начинай с UNION ALL. Используй UNION только если действительно нужно убрать дубликаты. Дедупликация стоит сортировки — это дорого на больших объёмах.
ETL-паттерн: incremental load¶
-- Загружаем новые записи, потом обновлённые — UNION ALL склеит
SELECT * FROM staging_orders WHERE is_new = true
UNION ALL
SELECT * FROM staging_orders WHERE is_updated = true;
UNION: объединить и убрать дубликаты¶
UNION = UNION ALL + DISTINCT. PostgreSQL сортирует результат для удаления дубликатов.
-- Все города, где есть клиенты ИЛИ доставка
SELECT city FROM customers
UNION
SELECT delivery_city FROM shipping_addresses;
Когда UNION полезен¶
-- Все уникальные клиенты, которые делали заказ ИЛИ оставляли отзыв
SELECT customer_id FROM orders
UNION
SELECT customer_id FROM reviews;
Без UNION (с ALL) клиент с 5 заказами и 3 отзывами появился бы 8 раз.
UNION ALL + DISTINCT vs UNION¶
-- Эквивалентные запросы:
SELECT city FROM customers UNION SELECT city FROM employees;
SELECT DISTINCT city FROM (
SELECT city FROM customers
UNION ALL
SELECT city FROM employees
) AS combined;
Первый вариант короче, но второй даёт больше контроля (можно добавить фильтры до дедупликации).
INTERSECT: только общие строки¶
INTERSECT оставляет строки, присутствующие в обоих наборах.
Клиенты, которые и заказывали, и оставляли отзывы¶
Города, где есть и клиенты, и сотрудники¶
INTERSECT ALL
INTERSECT ALL сохраняет дубликаты с учётом кратности. Если значение X встречается 3 раза в первом наборе и 2 раза во втором — INTERSECT ALL вернёт X дважды. На практике используется редко.
EXCEPT: вычитание наборов¶
EXCEPT возвращает строки из первого набора, которых нет во втором. Порядок запросов важен!
Клиенты без отзывов¶
-- Кто заказывал, но ни разу не оставил отзыв
SELECT customer_id FROM orders
EXCEPT
SELECT customer_id FROM reviews;
Товары, которые ни разу не покупали¶
EXCEPT vs NOT EXISTS¶
-- EXCEPT: убирает дубликаты автоматически
SELECT customer_id FROM orders
EXCEPT
SELECT customer_id FROM reviews;
-- NOT EXISTS: сохраняет дубликаты, гибче для сложных условий
SELECT DISTINCT o.customer_id
FROM orders o
WHERE NOT EXISTS (
SELECT 1 FROM reviews r WHERE r.customer_id = o.customer_id
);
| Подход | Дубликаты | NULL | Гибкость |
|---|---|---|---|
EXCEPT |
Убирает автоматически | Корректно | Только по всем колонкам |
NOT EXISTS |
Нужен DISTINCT |
Корректно | Любые условия связи |
NOT IN |
Нужен DISTINCT |
Ловушка с NULL! | Только по одной колонке |
NOT IN + NULL
Если подзапрос в NOT IN возвращает хотя бы один NULL, результат всегда пуст. Используй NOT EXISTS или EXCEPT.
Сверка данных: ETL-паттерн¶
Одна из главных задач DE — сверить данные между витринами. EXCEPT идеален для этого:
-- Строки, которые есть в OLTP (orders), но отсутствуют в DWH (fact_sales)
SELECT order_id FROM orders WHERE status = 'delivered'
EXCEPT
SELECT order_id FROM fact_sales;
-- Строки, которые есть в DWH, но отсутствуют в OLTP (осиротевшие факты)
SELECT order_id FROM fact_sales
EXCEPT
SELECT order_id FROM orders WHERE status = 'delivered';
Для полной сверки — оба запроса через UNION ALL:
SELECT 'missing_in_dwh' AS issue, order_id FROM (
SELECT order_id FROM orders WHERE status = 'delivered'
EXCEPT
SELECT order_id FROM fact_sales
) AS missing
UNION ALL
SELECT 'orphaned_in_dwh' AS issue, order_id FROM (
SELECT order_id FROM fact_sales
EXCEPT
SELECT order_id FROM orders WHERE status = 'delivered'
) AS orphaned;
Комбинирование нескольких операций¶
Операции можно цепочить. Приоритет: INTERSECT > UNION = EXCEPT. Используй скобки для явности:
-- Клиенты, которые заказывали И оставляли отзыв, НО не отменяли заказы
(
SELECT customer_id FROM orders WHERE status = 'delivered'
INTERSECT
SELECT customer_id FROM reviews
)
EXCEPT
SELECT customer_id FROM orders WHERE status = 'cancelled';
ORDER BY и LIMIT с операциями над наборами¶
ORDER BY и LIMIT применяются ко всему результату и пишутся один раз в конце:
SELECT name, 'customer' AS type FROM customers
UNION ALL
SELECT name, 'employee' AS type FROM employees
ORDER BY name -- ← сортирует весь объединённый результат
LIMIT 10; -- ← ограничивает весь результат
Если нужно отсортировать или ограничить отдельный запрос — оберни его в подзапрос:
(SELECT name FROM customers ORDER BY name LIMIT 5)
UNION ALL
(SELECT name FROM employees ORDER BY name LIMIT 5);
Производительность¶
| Оператор | Стоимость | Почему |
|---|---|---|
UNION ALL |
Минимальная | Просто конкатенация |
UNION |
Средняя | Сортировка/хеш для дедупликации |
INTERSECT |
Средняя | Сортировка/хеш для поиска пересечений |
EXCEPT |
Средняя | Сортировка/хеш для поиска разницы |
EXPLAIN ANALYZE
PostgreSQL может использовать HashSetOp (хеш-таблица) или Sort + SetOp (сортировка) для UNION/INTERSECT/EXCEPT. На маленьких таблицах разницы нет, на больших — проверяй через EXPLAIN ANALYZE.
Типичные ошибки¶
1. UNION вместо UNION ALL¶
-- Медленно и неправильно: теряешь дубликаты в логе событий
SELECT customer_id, event_date FROM orders
UNION -- ← убирает дубликаты (не нужно!)
SELECT customer_id, event_date FROM events;
-- Правильно:
... UNION ALL ...
2. Разное количество колонок¶
-- Ошибка: первый запрос возвращает 3 колонки, второй — 2
SELECT name, email, city FROM customers
UNION ALL
SELECT name, email FROM employees;
-- Правильно: добавь NULL
SELECT name, email, city FROM customers
UNION ALL
SELECT name, email, NULL AS city FROM employees;
3. Несовместимые типы¶
-- Потенциальная проблема: price (numeric) vs name (text)
SELECT price FROM products
UNION ALL
SELECT name FROM customers; -- ← ошибка типов
-- Приведи явно:
SELECT price::text FROM products
UNION ALL
SELECT name FROM customers;
Шпаргалка¶
| Задача | Оператор |
|---|---|
| Склеить данные из нескольких таблиц | UNION ALL |
| Собрать уникальные значения из двух источников | UNION |
| Найти общие записи | INTERSECT |
| Найти записи, которых нет в другом наборе | EXCEPT |
| Сверить OLTP и DWH | EXCEPT в обе стороны |
| Единый поток событий | UNION ALL + ORDER BY |
Что запомнить¶
UNION ALL— самый быстрый: склеивает без дедупликации. Используй по умолчаниюUNION=UNION ALL+DISTINCT. Дороже, но убирает дубликатыINTERSECT— пересечение двух выборок (общие строки)EXCEPT— вычитание: строки из первого набора, которых нет во втором- Все операторы требуют одинаковое количество колонок с совместимыми типами
ORDER BY/LIMIT— один раз в конце, применяются ко всему результатуEXCEPT— идеален для сверки данных между витринами (ETL)NOT IN+ NULL = пустой результат. ИспользуйEXCEPTилиNOT EXISTS- Приоритет:
INTERSECTвыполняется раньшеUNION/EXCEPT
Попробуй сам¶
Проверь себя¶
Что дальше?¶
Теперь ты умеешь комбинировать выборки. Следующий шаг — научиться безопасно изменять данные и работать с транзакциями. А если нужно вернуться к аналитике — перечитай оконные функции.
Источники¶
- PostgreSQL: UNION, INTERSECT, EXCEPT — синтаксис, правила совместимости типов, приоритет операций
- PostgreSQL: Combining Queries — примеры комбинирования запросов