sqlpostgresqlintersectset-operators

SQL INTERSECT: Rows Present in Both Queries

How the INTERSECT operator finds rows common to two queries, how it differs from INTERSECT ALL, and when a JOIN or EXISTS fits better.

11 min läsningReferencesql · postgresql · intersect · set-operators · mysql
Den här artikeln finns för närvarande på ryska — en engelsk översättning är på gång.

В SQL есть операции, которые работают не с отдельными строками, а с целыми наборами строк.

Например:

  • UNION объединяет две выборки;
  • EXCEPT показывает строки, которые есть в первой выборке, но отсутствуют во второй;
  • INTERSECT показывает строки, которые есть сразу в обеих выборках.

INTERSECT удобно использовать, когда вопрос звучит так:

Что есть и там, и там?

Например:

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

Главная идея простая:

INTERSECT возвращает пересечение двух результатов SELECT.

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

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

Представим две выборки.

Первая — пользователи, которые сделали заказ:

user_id
-------
1
2
3
5

Вторая — пользователи, которые запросили возврат:

user_id
-------
2
3
4

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

Это:

2
3

Именно такую задачу решает INTERSECT.

SELECT user_id
FROM purchases

INTERSECT

SELECT user_id
FROM refunds;

Результат:

user_id
-------
2
3

Можно представить INTERSECT как пересечение двух кругов:

Первый круг: пользователи с покупками
Второй круг: пользователи с возвратами

INTERSECT: пользователи, которые попали в оба круга

Базовый синтаксис INTERSECT

Синтаксис выглядит так:

SELECT column_1, column_2
FROM table_1

INTERSECT

SELECT column_1, column_2
FROM table_2;

Важно: оба SELECT должны возвращать одинаковое количество колонок.

Например, так можно:

SELECT email
FROM users

INTERSECT

SELECT email
FROM subscribers;

А так нельзя:

SELECT id, email
FROM users

INTERSECT

SELECT email
FROM subscribers;

Почему нельзя?

Потому что первый запрос возвращает две колонки:

id, email

А второй — одну:

email

База не сможет сравнить такие строки между собой.

Пример: 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, которые есть и среди пользователей, и среди подписчиков.

SELECT email
FROM users

INTERSECT

SELECT email
FROM subscribers;

Результат:

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

anna@mail.com есть только в users, поэтому она не попала в результат.

tom@mail.com есть только в subscribers, поэтому он тоже не попал.

А bob@mail.com и kate@mail.com есть в обеих выборках, поэтому INTERSECT их вернул.

INTERSECT сравнивает строки целиком

Очень важный момент: INTERSECT сравнивает не одну «главную» колонку, а всю строку целиком.

Например:

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

INTERSECT

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

На первый взгляд email совпадает. Но строки целиком разные, потому что отличается колонка source.

Первая строка:

anna@mail.com | users

Вторая строка:

anna@mail.com | leads

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

А вот так строки совпадают:

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

INTERSECT

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

Результат:

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

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

INTERSECT сравнивает все выбранные колонки, а не только первую.

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

SELECT email
FROM users

INTERSECT

SELECT email
FROM leads;

Если добавите source, created_at, status или другие колонки, они тоже будут участвовать в сравнении.

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

INTERSECT сравнивает первую колонку с первой, вторую со второй, третью с третьей и так далее.

Например:

SELECT id, email
FROM users

INTERSECT

SELECT user_id, email
FROM orders;

Здесь база сравнивает:

users.id       с orders.user_id
users.email    с orders.email

А если случайно поменять порядок:

SELECT id, email
FROM users

INTERSECT

SELECT email, user_id
FROM orders;

смысл запроса сломается: база будет пытаться сравнивать id с email, а email с user_id.

Иногда такой запрос сразу упадёт из-за несовместимых типов. А иногда выполнится, но результат будет бессмысленным.

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

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

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

Например, такой запрос логичен:

SELECT email
FROM users

INTERSECT

SELECT email
FROM subscribers;

В обеих выборках одна текстовая колонка.

А вот здесь может понадобиться явное приведение типа:

SELECT id::text AS value
FROM users

INTERSECT

SELECT external_id AS value
FROM imported_users;

Если id в одной таблице числовой, а external_id в другой текстовый, лучше явно привести типы, чтобы запрос читался предсказуемо.

Пример:

SELECT id::text AS user_key
FROM users

INTERSECT

SELECT external_id::text AS user_key
FROM imported_users;

Так мы явно говорим базе:

Сравнивай оба значения как текстовые ключи.

INTERSECT убирает дубликаты

Обычный INTERSECT работает как операция над множествами. А в множестве одинаковое значение хранится один раз.

Например:

SELECT 'SQL' AS course
INTERSECT
SELECT 'SQL' AS course;

Результат:

course
------
SQL

Теперь пример с дублями:

SELECT user_id
FROM orders
WHERE status = 'paid'

INTERSECT

SELECT user_id
FROM refunds;

Если пользователь 7 встречается в заказах много раз и в возвратах несколько раз, обычный INTERSECT всё равно вернёт его один раз.

Пример:

Первая выборка:

user_id
-------
7
7
7
8

Вторая выборка:

user_id
-------
7
7
9

Обычный INTERSECT вернёт:

user_id
-------
7

То есть он отвечает на вопрос:

Какие значения встречаются в обеих выборках?

А не на вопрос:

Сколько раз они там встречаются?

INTERSECT ALL: пересечение с учётом повторов

У INTERSECT есть вариант:

INTERSECT ALL

Он сохраняет повторы, но не все подряд, а по особому правилу.

INTERSECT ALL смотрит, сколько раз строка встретилась слева, сколько раз справа, и возвращает минимальное количество повторов.

Например:

Первая выборка:

user_id
-------
7
7
7
8

Вторая выборка:

user_id
-------
7
7
9

user_id = 7 встретился:

слева  — 3 раза
справа — 2 раза

Значит INTERSECT ALL вернёт его 2 раза.

user_id
-------
7
7

Пример запроса:

SELECT user_id
FROM orders
WHERE amount > 100

INTERSECT ALL

SELECT user_id
FROM orders
WHERE status = 'paid';

Обычный INTERSECT вернул бы каждого подходящего пользователя один раз.

INTERSECT ALL может вернуть одну и ту же строку несколько раз, если она несколько раз встречается в обеих выборках.

На практике чаще используют обычный INTERSECT, потому что обычно важен сам факт пересечения:

пользователь есть и в первой группе, и во второй

А не количество повторов.

Пример: пользователи, которые и заказывали, и делали возврат

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

id | user_id | status
---+---------+----------
1  | 1       | paid
2  | 2       | paid
3  | 3       | paid
4  | 4       | cancelled

И таблица refunds:

id | user_id | created_at
---+---------+---------------------
1  | 2       | 2026-06-01 10:00:00
2  | 3       | 2026-06-02 12:00:00
3  | 5       | 2026-06-03 14:00:00

Нужно найти пользователей, у которых есть оплаченный заказ и есть возврат.

SELECT user_id
FROM orders
WHERE status = 'paid'

INTERSECT

SELECT user_id
FROM refunds;

Результат:

user_id
-------
2
3

Пользователь 1 делал заказ, но не делал возврат.

Пользователь 5 делал возврат, но в оплаченных заказах его нет.

А пользователи 2 и 3 есть в обеих выборках.

Пример: страны, где есть и пользователи, и оплаченные заказы

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

id | email          | country
---+----------------+---------
1  | anna@mail.com  | Vietnam
2  | bob@mail.com   | Germany
3  | kate@mail.com  | France
4  | tom@mail.com   | Spain

И таблица orders:

id | user_id | amount | status
---+---------+--------+--------
1  | 1       | 1500   | paid
2  | 2       | 2300   | paid
3  | 3       | 900    | failed

Хотим найти страны, в которых есть пользователи с оплаченными заказами.

SELECT country
FROM users

INTERSECT

SELECT u.country
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid';

Результат:

country
--------
Vietnam
Germany

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

Vietnam
Germany
France
Spain

Вторая выборка возвращает страны пользователей, у которых есть оплаченный заказ:

Vietnam
Germany

INTERSECT оставляет только то, что есть в обеих выборках.

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

INTERSECT и NULL

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

Например:

SELECT NULL = NULL;

не возвращает TRUE.

Но в операциях над множествами, таких как INTERSECT, строки с NULL в одинаковых позициях считаются совпадающими.

Пример:

SELECT NULL AS country

INTERSECT

SELECT NULL AS country;

Результат:

country
-------
NULL

То есть для целей пересечения две строки с NULL считаются одинаковыми.

Это важно, потому что обычный JOIN по условию:

a.country = b.country

не соединит строки, где country равен NULL с обеих сторон.

А INTERSECT такие строки пересечёт.

Пример:

SELECT country
FROM users

INTERSECT

SELECT country
FROM archived_users;

Если в обеих выборках есть строка с country = NULL, она попадёт в результат.

INTERSECT против INNER JOIN

Иногда ту же задачу можно решить через INNER JOIN.

Например, хотим найти пользователей, которые есть и в orders, и в refunds.

Вариант через INTERSECT:

SELECT user_id
FROM orders

INTERSECT

SELECT user_id
FROM refunds;

Вариант через JOIN:

SELECT DISTINCT o.user_id
FROM orders o
JOIN refunds r ON r.user_id = o.user_id;

Оба запроса могут вернуть похожий результат.

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

INTERSECT работает с двумя готовыми наборами строк и возвращает их пересечение.

JOIN соединяет строки таблиц между собой.

Если у пользователя 3 заказа и 2 возврата, JOIN может создать 6 строк:

3 заказа × 2 возврата = 6 комбинаций

Поэтому часто приходится добавлять DISTINCT.

SELECT DISTINCT o.user_id
FROM orders o
JOIN refunds r ON r.user_id = o.user_id;

А INTERSECT автоматически вернёт уникальный список, если используется обычная форма без ALL.

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

INTERSECT — когда нужно пересечение наборов. JOIN — когда нужно соединить строки и взять данные из обеих таблиц.

INTERSECT против EXISTS

Ещё один способ найти строки, которые есть в другой таблице, — использовать EXISTS.

Например:

SELECT DISTINCT o.user_id
FROM orders o
WHERE EXISTS (
  SELECT 1
  FROM refunds r
  WHERE r.user_id = o.user_id
);

Этот запрос читается так:

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

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

Например, хотим вывести сами заказы пользователей, у которых есть возвраты:

SELECT o.id, o.user_id, o.amount, o.created_at
FROM orders o
WHERE EXISTS (
  SELECT 1
  FROM refunds r
  WHERE r.user_id = o.user_id
);

Через INTERSECT такую задачу выразить менее удобно, потому что INTERSECT должен сравнивать одинаковые наборы колонок.

Поэтому выбор такой:

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

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

INTERSECT хорошо подходит, когда вы мыслите именно наборами.

Например:

Набор A: пользователи, которые сделали заказ
Набор B: пользователи, которые запросили возврат

Нужно: пользователи, которые есть и в A, и в B

Или:

Набор A: email из новой CRM
Набор B: email из старой CRM

Нужно: email, которые есть в обеих системах

В таких задачах INTERSECT делает запрос коротким и читаемым.

Пример:

SELECT email
FROM old_crm_users

INTERSECT

SELECT email
FROM new_crm_users;

Сразу понятно, что мы ищем пересечение двух списков.

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

INTERSECT не всегда лучший выбор.

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

Например:

SELECT u.id, u.email, u.created_at
FROM users u
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
);

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

Если нужны данные из обеих таблиц, используйте JOIN.

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

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

Требования к SELECT внутри INTERSECT

У запросов внутри INTERSECT должны совпадать:

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

Корректный пример:

SELECT email, country
FROM users

INTERSECT

SELECT email, country
FROM archived_users;

Некорректный пример по количеству колонок:

SELECT id, email, country
FROM users

INTERSECT

SELECT email, country
FROM archived_users;

Некорректный пример по смыслу колонок:

SELECT id, email
FROM users

INTERSECT

SELECT email, id
FROM archived_users;

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

ORDER BY при INTERSECT

Как и в случае с UNION, итоговый ORDER BY пишется в самом конце и относится ко всему результату.

Например:

SELECT email
FROM users

INTERSECT

SELECT email
FROM subscribers

ORDER BY email;

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

Важно: без ORDER BY порядок строк не гарантирован.

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

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

Нужен порядок — пишите ORDER BY.

LIMIT при INTERSECT

LIMIT в конце тоже применяется ко всему результату.

SELECT email
FROM users

INTERSECT

SELECT email
FROM subscribers

ORDER BY email
LIMIT 10;

Этот запрос:

  1. найдёт email, которые есть в обеих таблицах;
  2. отсортирует результат;
  3. вернёт первые 10 строк.

Если нужно ограничить одну из веток до пересечения, используйте подзапрос.

Например:

SELECT email
FROM (
  SELECT email
  FROM users
  ORDER BY created_at DESC
  LIMIT 100
) AS recent_users

INTERSECT

SELECT email
FROM subscribers;

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

Приоритет INTERSECT среди операций множеств

В SQL можно комбинировать разные операции:

UNION
INTERSECT
EXCEPT

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

Например:

SELECT email FROM users

UNION

SELECT email FROM leads

INTERSECT

SELECT email FROM subscribers;

В PostgreSQL INTERSECT имеет более высокий приоритет, чем UNION и EXCEPT.

То есть такой запрос будет восприниматься примерно так:

SELECT email FROM users

UNION

(
  SELECT email FROM leads
  INTERSECT
  SELECT email FROM subscribers
);

А не так:

(
  SELECT email FROM users
  UNION
  SELECT email FROM leads
)

INTERSECT

SELECT email FROM subscribers;

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

Например:

(
  SELECT email FROM users
  UNION
  SELECT email FROM leads
)

INTERSECT

SELECT email FROM subscribers;

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

INTERSECT и производительность

INTERSECT должен сравнить результаты двух выборок и понять, какие строки есть в обеих.

Для этого базе может понадобиться:

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

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

На больших таблицах важно понимать, что INTERSECT — не магия. Он тоже выполняет работу, и иногда эта работа может быть дорогой.

В PostgreSQL можно посмотреть план выполнения через:

EXPLAIN
SELECT email FROM users
INTERSECT
SELECT email FROM subscribers;

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

EXPLAIN ANALYZE
SELECT email FROM users
INTERSECT
SELECT email FROM subscribers;

Если запрос тяжёлый, иногда стоит сравнить варианты:

  • INTERSECT;
  • EXISTS;
  • JOIN;
  • временные таблицы;
  • индексы по ключам сравнения.

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

Пример: найти общих клиентов в двух системах

Допустим, компания переезжает со старой CRM на новую.

Есть таблица old_crm_users:

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

И таблица new_crm_users:

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

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

SELECT email
FROM old_crm_users

INTERSECT

SELECT email
FROM new_crm_users;

Результат:

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

Такой запрос очень хорошо подходит для сверки миграций.

Пример: найти товары, которые есть в двух каталогах

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

SELECT sku
FROM old_catalog

INTERSECT

SELECT sku
FROM new_catalog;

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

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

SELECT sku
FROM old_catalog

EXCEPT

SELECT sku
FROM new_catalog;

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

SELECT sku
FROM old_catalog

UNION

SELECT sku
FROM new_catalog;

Так удобно запомнить три операции:

UNION     — всё из двух выборок
INTERSECT — только общее
EXCEPT    — только то, что есть в первой, но нет во второй

INTERSECT в PostgreSQL

В PostgreSQL INTERSECT поддерживается полноценно.

Можно использовать:

INTERSECT

и:

INTERSECT ALL

Обычный INTERSECT удаляет дубликаты.

INTERSECT ALL учитывает количество повторов и возвращает минимальное число одинаковых строк, найденных в обеих выборках.

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

INTERSECT в MySQL

В MySQL оператор INTERSECT появился не сразу.

В современных версиях MySQL он поддерживается, начиная с MySQL 8.0.31.

Пример:

SELECT email
FROM users

INTERSECT

SELECT email
FROM subscribers;

В более старых версиях MySQL INTERSECT обычно имитировали через INNER JOIN, IN или EXISTS.

Например, через IN:

SELECT DISTINCT email
FROM users
WHERE email IN (
  SELECT email
  FROM subscribers
);

Или через EXISTS:

SELECT DISTINCT u.email
FROM users u
WHERE EXISTS (
  SELECT 1
  FROM subscribers s
  WHERE s.email = u.email
);

Если вы работаете с MySQL, важно проверить версию сервера перед использованием INTERSECT.

INTERSECT в ClickHouse

В ClickHouse тоже можно работать с операциями множеств, включая пересечение выборок.

Пример:

SELECT email
FROM users

INTERSECT

SELECT email
FROM subscribers;

Также в ClickHouse можно явно указывать режимы вроде INTERSECT DISTINCT или INTERSECT ALL, если они поддерживаются вашей версией и настройками.

Для аналитических задач в ClickHouse часто используют и другие подходы: JOIN, IN, агрегации, временные подзапросы. Выбор зависит от объёма данных, структуры таблиц и конкретной задачи.

Практическое правило остаётся тем же:

Если нужно пересечение двух наборов строк — смотрите в сторону INTERSECT. Если нужны дополнительные поля или сложная логика соединения — используйте JOIN или EXISTS.

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

Найти email, которые есть в двух таблицах

SELECT email
FROM users

INTERSECT

SELECT email
FROM subscribers;

Найти пользователей, которые и покупали, и делали возврат

SELECT user_id
FROM orders
WHERE status = 'paid'

INTERSECT

SELECT user_id
FROM refunds;

Найти общие товары в двух каталогах

SELECT sku
FROM old_catalog

INTERSECT

SELECT sku
FROM new_catalog;

Найти общие страны в двух выборках

SELECT country
FROM users

INTERSECT

SELECT country
FROM archived_users;

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

SELECT email, country
FROM users

INTERSECT

SELECT email, country
FROM imported_users;

Пересечение с учётом повторов

SELECT user_id
FROM orders

INTERSECT ALL

SELECT user_id
FROM refunds;

Альтернатива через EXISTS

SELECT DISTINCT u.email
FROM users u
WHERE EXISTS (
  SELECT 1
  FROM subscribers s
  WHERE s.email = u.email
);

Явная сортировка результата

SELECT email
FROM users

INTERSECT

SELECT email
FROM subscribers

ORDER BY email;

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

INTERSECT возвращает строки, которые есть в обеих выборках.

Пример:

SELECT email FROM users
INTERSECT
SELECT email FROM subscribers;

Он отвечает на вопрос:

Какие email есть и в users, и в subscribers?

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

  • оба SELECT должны возвращать одинаковое количество колонок;
  • типы колонок должны быть совместимы;
  • порядок колонок важен;
  • сравнивается вся строка целиком;
  • обычный INTERSECT убирает дубликаты;
  • INTERSECT ALL учитывает повторы;
  • строки с NULL в одинаковых местах считаются совпадающими;
  • порядок результата не гарантирован без ORDER BY.

INTERSECT удобен, когда вы сравниваете два набора и хотите получить их общую часть.

Если нужны дополнительные колонки из основной таблицы, часто удобнее EXISTS.

Если нужны данные из обеих таблиц, обычно нужен JOIN.

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

INTERSECT — это оператор SQL для поиска пересечения двух выборок.

Он показывает только те строки, которые присутствуют и в первом SELECT, и во втором SELECT.

Например:

SELECT user_id FROM orders
INTERSECT
SELECT user_id FROM refunds;

Такой запрос вернёт пользователей, которые есть в обеих выборках.

Можно запомнить очень просто:

UNION     — сложить выборки
INTERSECT — найти общее
EXCEPT    — вычесть одну выборку из другой

INTERSECT особенно хорош в задачах сверки данных: сравнить две CRM, два каталога, два списка пользователей, два источника событий.

Если вопрос звучит как «что есть и там, и там», INTERSECT часто даёт самый короткий и понятный SQL-запрос.

Öva på riktiga uppgifter

Lös uppgifter i SQL-tränaren med omedelbar rättning och ledtrådar.

Öppna tränaren