INTERSECT vs EXCEPT в SQL

Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.

Зачем это знать

INTERSECT и EXCEPT — малоизвестные операторы SQL, которые экономят 10 строк кода при правильном использовании. На собесе для middle-аналитика могут спросить: «как найти пользователей, которые есть в обеих таблицах без JOIN». Ответ — INTERSECT.

Большинство аналитиков пользуются INNER JOIN + DISTINCT вместо INTERSECT. Работает, но длиннее и иногда медленнее. Знание set-операций = признак продвинутого SQL-скилла.

В статье:

  • Короткий ответ
  • INTERSECT — пересечение
  • EXCEPT / MINUS — разность
  • UNION — объединение
  • Примеры использования
  • Эквивалентные записи через JOIN

Короткий ответ

  • UNION: все уникальные значения из обеих
  • INTERSECT: только те, что есть в обеих
  • EXCEPT (MINUS): те, что в первой, но нет во второй

Работают как set operations — автоматически дедуплицируют.

INTERSECT

Пересечение — значения в обеих таблицах:

SELECT email FROM users_old
INTERSECT
SELECT email FROM users_new;

Вернёт emails, которые есть и в старой базе, и в новой.

Пример

-- пользователи, купившие И товар A, И товар B
SELECT user_id FROM orders WHERE product_id = 'A'
INTERSECT
SELECT user_id FROM orders WHERE product_id = 'B';

EXCEPT

Разность — в первой, но не во второй:

SELECT email FROM users
EXCEPT
SELECT email FROM unsubscribed;

Вернёт emails пользователей, которые не отписались.

Пример

-- товары, которые есть в каталоге, но не продавались в 2026
SELECT product_id FROM products
EXCEPT
SELECT DISTINCT product_id FROM orders WHERE created_at >= '2026-01-01';

В Oracle

SELECT ... FROM t1
MINUS
SELECT ... FROM t2;

MINUS = EXCEPT.

UNION

SELECT email FROM customers
UNION
SELECT email FROM vendors;

UNION vs UNION ALL

UNION      -- уникальные (дедупликация, медленнее)
UNION ALL  -- всё (с дублями, быстрее)

Требования

Для всех set-операций:

  • Одинаковое количество колонок
  • Совместимые типы (или CAST)
  • Порядок колонок важен (сравниваются позиционно, не по имени)
-- ошибка — разное количество колонок
SELECT a, b FROM t1 INTERSECT SELECT a FROM t2;

-- ошибка типов
SELECT name FROM t1 INTERSECT SELECT id FROM t2;  -- string vs int

Эквиваленты через JOIN

INTERSECT через INNER JOIN

SELECT DISTINCT a.email
FROM users_old a
INNER JOIN users_new b ON b.email = a.email;

EXCEPT через LEFT JOIN

SELECT DISTINCT a.email
FROM users a
LEFT JOIN unsubscribed b ON b.email = a.email
WHERE b.email IS NULL;

EXCEPT через NOT EXISTS

SELECT DISTINCT email FROM users u
WHERE NOT EXISTS (
    SELECT 1 FROM unsubscribed WHERE email = u.email
);

Когда set-операции лучше JOIN

Читаемость

-- Clear intent
SELECT product_id FROM catalog
EXCEPT
SELECT product_id FROM sold_products;

-- vs
SELECT c.product_id FROM catalog c
LEFT JOIN sold_products s ON s.product_id = c.product_id
WHERE s.product_id IS NULL;

Multi-column comparison

SELECT user_id, product_id FROM purchases_old
INTERSECT
SELECT user_id, product_id FROM purchases_new;

JOIN эквивалент — 3 строки более.

Auto-deduplication

INTERSECT / EXCEPT сразу убирают дубликаты. JOIN + DISTINCT.

Performance

  • INTERSECT / EXCEPT обычно медленнее INNER JOIN / NOT EXISTS, но разница маленькая
  • Для больших данных — optimizer может переписать в JOIN
  • Используйте то, что читается чище

В разных СУБД

Postgres MySQL MSSQL Oracle
UNION
INTERSECT ✓ (8.0+)
EXCEPT ✓ (8.0+) MINUS

В MySQL до 8.0 — только через NOT IN / NOT EXISTS.

Практические use cases

Миграция данных

-- какие users в старой базе, но не в новой (нужно смигрировать)
SELECT user_id FROM old_users
EXCEPT
SELECT user_id FROM new_users;

A/B-test audience

-- пользователи в test, но не в control (не должно быть)
SELECT user_id FROM test_group
INTERSECT
SELECT user_id FROM control_group;

Должно быть 0. Если не 0 — sample contamination.

Cross-sell analysis

-- купили A, но не B
SELECT DISTINCT user_id FROM orders WHERE product = 'A'
EXCEPT
SELECT DISTINCT user_id FROM orders WHERE product = 'B';

Частые ошибки

1. INTERSECT ALL / EXCEPT ALL

В Postgres есть INTERSECT ALL / EXCEPT ALL — сохраняют дубликаты. Обычно не нужно.

2. NULL handling

NULL = NULL → UNKNOWN. В set-операциях NULL сравниваются как равные. Это отличие от JOIN.

-- в INTERSECT NULL=NULL считаются равными
SELECT NULL INTERSECT SELECT NULL;  -- возвращает NULL ROW

3. Порядок колонок

Сравниваются позиционно. (a, b) INTERSECT (b, a) — разные результаты.

4. В MySQL < 8.0

Нет INTERSECT / EXCEPT. Используйте NOT EXISTS / INNER JOIN.

Связанные темы

FAQ

INTERSECT или INNER JOIN?

Functionally эквивалентны. INTERSECT короче для simple case.

EXCEPT с NULL?

В set-операциях NULL считаются равными между собой. Отличие от JOIN.

MySQL старых версий?

Используйте NOT IN / NOT EXISTS.

Performance?

Обычно на уровне JOIN. Optimizer переписывает.


Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.