Ta članek je trenutno v ruščini — angleški prevod je v pripravi.
В 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;
Этот запрос:
- найдёт email, которые есть в обеих таблицах;
- отсортирует результат;
- вернёт первые 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-запрос.
В SQL есть операции, которые работают не с отдельными строками, а с целыми наборами строк.
Например:
UNIONобъединяет две выборки;EXCEPTпоказывает строки, которые есть в первой выборке, но отсутствуют во второй;INTERSECTпоказывает строки, которые есть сразу в обеих выборках.INTERSECTудобно использовать, когда вопрос звучит так:Например:
Главная идея простая:
То есть он оставляет только те строки, которые одновременно присутствуют в первой и во второй выборке.
Что делает INTERSECT простыми словами
Представим две выборки.
Первая — пользователи, которые сделали заказ:
Вторая — пользователи, которые запросили возврат:
Если мы хотим найти пользователей, которые и сделали заказ, и запросили возврат, нам нужны значения, которые есть в обоих списках.
Это:
Именно такую задачу решает
INTERSECT.SELECT user_id FROM purchases INTERSECT SELECT user_id FROM refunds;Результат:
Можно представить
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;Почему нельзя?
Потому что первый запрос возвращает две колонки:
А второй — одну:
База не сможет сравнить такие строки между собой.
Пример: email, которые есть в двух таблицах
Допустим, есть таблица
users:И таблица
subscribers:Нужно найти email, которые есть и среди пользователей, и среди подписчиков.
SELECT email FROM users INTERSECT SELECT email FROM subscribers;Результат:
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.Первая строка:
Вторая строка:
Результат будет пустым, потому что строки не совпадают полностью.
А вот так строки совпадают:
SELECT 'anna@mail.com' AS email, 'users' AS source INTERSECT SELECT 'anna@mail.com' AS email, 'users' AS source;Результат:
Главное правило:
Если вам нужно сравнить только 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;Здесь база сравнивает:
А если случайно поменять порядок:
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;Результат:
Теперь пример с дублями:
SELECT user_id FROM orders WHERE status = 'paid' INTERSECT SELECT user_id FROM refunds;Если пользователь
7встречается в заказах много раз и в возвратах несколько раз, обычныйINTERSECTвсё равно вернёт его один раз.Пример:
Первая выборка:
Вторая выборка:
Обычный
INTERSECTвернёт:То есть он отвечает на вопрос:
А не на вопрос:
INTERSECT ALL: пересечение с учётом повторов
У
INTERSECTесть вариант:INTERSECT ALLОн сохраняет повторы, но не все подряд, а по особому правилу.
INTERSECT ALLсмотрит, сколько раз строка встретилась слева, сколько раз справа, и возвращает минимальное количество повторов.Например:
Первая выборка:
Вторая выборка:
user_id = 7встретился:Значит
INTERSECT ALLвернёт его 2 раза.Пример запроса:
SELECT user_id FROM orders WHERE amount > 100 INTERSECT ALL SELECT user_id FROM orders WHERE status = 'paid';Обычный
INTERSECTвернул бы каждого подходящего пользователя один раз.INTERSECT ALLможет вернуть одну и ту же строку несколько раз, если она несколько раз встречается в обеих выборках.На практике чаще используют обычный
INTERSECT, потому что обычно важен сам факт пересечения:А не количество повторов.
Пример: пользователи, которые и заказывали, и делали возврат
Допустим, есть таблица
orders:И таблица
refunds:Нужно найти пользователей, у которых есть оплаченный заказ и есть возврат.
SELECT user_id FROM orders WHERE status = 'paid' INTERSECT SELECT user_id FROM refunds;Результат:
Пользователь
1делал заказ, но не делал возврат.Пользователь
5делал возврат, но в оплаченных заказах его нет.А пользователи
2и3есть в обеих выборках.Пример: страны, где есть и пользователи, и оплаченные заказы
Допустим, есть таблица
users:И таблица
orders:Хотим найти страны, в которых есть пользователи с оплаченными заказами.
SELECT country FROM users INTERSECT SELECT u.country FROM users u JOIN orders o ON o.user_id = u.id WHERE o.status = 'paid';Результат:
Первая выборка возвращает все страны пользователей:
Вторая выборка возвращает страны пользователей, у которых есть оплаченный заказ:
INTERSECTоставляет только то, что есть в обеих выборках.В этом конкретном примере первая выборка выглядит лишней, потому что оплаченные заказы и так связаны с пользователями. Но сам пример хорошо показывает механику: мы пересекаем два набора стран.
INTERSECT и NULL
В обычных сравнениях SQL
NULLведёт себя непривычно.Например:
SELECT NULL = NULL;не возвращает
TRUE.Но в операциях над множествами, таких как
INTERSECT, строки сNULLв одинаковых позициях считаются совпадающими.Пример:
SELECT NULL AS country INTERSECT SELECT NULL AS country;Результат:
То есть для целей пересечения две строки с
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 строк:Поэтому часто приходится добавлять
DISTINCT.SELECT DISTINCT o.user_id FROM orders o JOIN refunds r ON r.user_id = o.user_id;А
INTERSECTавтоматически вернёт уникальный список, если используется обычная форма безALL.Можно запомнить так:
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
INTERSECTхорошо подходит, когда вы мыслите именно наборами.Например:
Или:
В таких задачах
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порядок строк не гарантирован.Даже если результат кажется отсортированным, полагаться на это нельзя.
Правило простое:
LIMIT при INTERSECT
LIMITв конце тоже применяется ко всему результату.SELECT email FROM users INTERSECT SELECT email FROM subscribers ORDER BY email LIMIT 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:И таблица
new_crm_users:Нужно найти клиентов, которые есть в обеих системах.
SELECT email FROM old_crm_users INTERSECT SELECT email FROM new_crm_users;Результат:
Такой запрос очень хорошо подходит для сверки миграций.
Пример: найти товары, которые есть в двух каталогах
Допустим, есть старый и новый каталог товаров.
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;Так удобно запомнить три операции:
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, агрегации, временные подзапросы. Выбор зависит от объёма данных, структуры таблиц и конкретной задачи.Практическое правило остаётся тем же:
Практические шаблоны
Найти 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;Он отвечает на вопрос:
Главные правила:
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;Такой запрос вернёт пользователей, которые есть в обеих выборках.
Можно запомнить очень просто:
INTERSECTособенно хорош в задачах сверки данных: сравнить две CRM, два каталога, два списка пользователей, два источника событий.Если вопрос звучит как «что есть и там, и там»,
INTERSECTчасто даёт самый короткий и понятный SQL-запрос.