EXISTS — это проверка «есть ли хотя бы одна строка, подходящая под условие». Возвращает TRUE или FALSE. Используется обычно в WHERE как «отфильтруй мне строки, у которых есть связанная запись».
Самый частый сценарий: «покажи клиентов, у которых хотя бы один заказ». Ты не хочешь сами заказы — только клиентов. И тебе всё равно, сколько у них заказов: один, десять, сто. Главное — есть.
Зачем нужен EXISTS
Альтернативы:
JOIN — соединить, потом DISTINCT чтобы убрать дубликаты. Работает, но избыточно тащит лишние данные.
IN (SELECT ...) — иногда работает, но проигрывает на больших данных и плохо обрабатывает NULL.
EXISTS — спроектирован именно для «есть/нет», без лишних движений.
Postgres умеет «остановиться» в подзапросе на первой найденной строке (EXISTS называют «short-circuit»). Это в разы быстрее, чем считать все связанные строки и потом схлопывать через DISTINCT.
Базовый синтаксис
SELECT *
FROM таблица_A a
WHERE EXISTS (
SELECT 1
FROM таблица_B b
WHERE b.something = a.something
);
Логика: для каждой строки a Postgres выполняет подзапрос. Если он вернул хотя бы одну строку — EXISTS = TRUE, строка a попадает в результат. Если ноль — FALSE, не попадает.
SELECT 1 внутри — это идиома. Что именно выбрать в подзапросе, неважно — Postgres всё равно проверяет только наличие, не содержимое. SELECT *, SELECT b.id, SELECT 'anything' — всё работает одинаково.
Пример с таблицами
customers:
| id |
name |
| 1 |
Аня |
| 2 |
Боб |
| 3 |
Вера |
| 4 |
Гриша |
orders:
| id |
customer_id |
amount |
| 1 |
1 |
100 |
| 2 |
1 |
250 |
| 3 |
3 |
80 |
Запрос «клиенты, у которых есть хотя бы один заказ»:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
Результат:
Анна (1) и Вера (3) есть в orders. Боб (2) и Гриша (4) — нет, отфильтровались.
Обрати внимание: Аня появилась один раз, хотя у неё два заказа. EXISTS не дублирует — он бинарный (есть/нет), не считает количество.
NOT EXISTS — обратная проверка
«Клиенты, у которых нет ни одного заказа» — NOT EXISTS:
SELECT *
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
Результат: Боб и Гриша.
Это типичный «anti-join»: «дай мне строки из A, у которых нет соответствия в B». Часто используется для поиска осиротевших данных, неиспользуемых аккаунтов, забытых тегов.
EXISTS vs IN
Часто их сравнивают. На простых сценариях оба работают:
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);
WHERE c.id IN (SELECT customer_id FROM orders);
Разница важна:
| Критерий |
EXISTS |
IN (subquery) |
| NULL в подзапросе |
Игнорирует |
NOT IN ломается на NULL |
| Производительность |
Обычно быстрее на больших данных |
Может тянуть всё в память |
| Условие на несколько колонок |
Естественно (WHERE a=A AND b=B) |
Не очень (WHERE (a, b) IN (SELECT ...)) |
Особо опасно: NOT IN (SELECT ...). Если в подзапросе хоть одна NULL — результат всего NOT IN становится UNKNOWN (= не TRUE), и ничего не выбирается. Это очень тонкий баг, ловит почти всех.
SELECT * FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);
SELECT * FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);
Правило простое: для anti-join всегда используй NOT EXISTS, а не NOT IN.
Условия из нескольких колонок
EXISTS хорошо работает на сложных условиях с несколькими полями:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.id
AND o.status = 'paid'
AND o.created_at > NOW() - INTERVAL '1 month'
);
Все условия внутри подзапроса. Снаружи — простое «есть/нет». Чисто и читаемо.
EXISTS в SELECT
Можно использовать EXISTS не только в WHERE, но и как boolean-значение в SELECT:
SELECT
c.id,
c.name,
EXISTS(SELECT 1 FROM orders o WHERE o.customer_id = c.id) AS has_orders
FROM customers c;
Получается дополнительная boolean-колонка. Удобно для отчётов «активные / неактивные».
Частые ошибки новичков
1. NOT IN с NULL в подзапросе. Главная ловушка. Уже разобрали — для anti-join всегда используй NOT EXISTS.
2. Не понимают коррелированный подзапрос. Внутри EXISTS обычно есть ссылка на внешнюю таблицу (o.customer_id = c.id). Это коррелированный подзапрос — он выполняется заново для каждой строки внешнего запроса. Postgres оптимизирует, но логически именно так.
3. SELECT * vs SELECT 1 внутри EXISTS. Без разницы. Postgres даже не посмотрит на список колонок — он только проверяет наличие. Принято писать SELECT 1 для ясности.
4. Делают EXISTS без коррелированной связи. WHERE EXISTS (SELECT 1 FROM orders) — без ссылки на внешнюю таблицу. Это вернёт TRUE для всех строк, если в orders вообще есть хоть одна запись. Скорее всего, не то что ты хотел.
5. JOIN + DISTINCT вместо EXISTS. Иногда работает, но избыточно. EXISTS чище и обычно быстрее, потому что не тащит данные.
6. Не пишут LIMIT 1 (внутри EXISTS). Не нужно — EXISTS сам останавливается на первой строке. SELECT 1 FROM ... WHERE ... LIMIT 1 ничего не ускорит, только запутает.
Мини-резюме
EXISTS (SELECT 1 FROM ... WHERE ...) — TRUE если подзапрос вернул хотя бы одну строку.
NOT EXISTS — наоборот, TRUE если ничего не нашлось. Для anti-join — единственно правильный способ.
- Внутри
EXISTS принято писать SELECT 1 — содержимое неважно.
- Для «существует ли связанная запись»
EXISTS обычно быстрее и чище, чем JOIN + DISTINCT.
NOT IN (SELECT ...) — опасно, если в подзапросе могут быть NULL. Используй NOT EXISTS.
- Корреляция (ссылка на внешнюю таблицу внутри подзапроса) — норма для
EXISTS.
EXISTS— это проверка «есть ли хотя бы одна строка, подходящая под условие». ВозвращаетTRUEилиFALSE. Используется обычно вWHEREкак «отфильтруй мне строки, у которых есть связанная запись».Самый частый сценарий: «покажи клиентов, у которых хотя бы один заказ». Ты не хочешь сами заказы — только клиентов. И тебе всё равно, сколько у них заказов: один, десять, сто. Главное — есть.
Зачем нужен EXISTS
Альтернативы:
JOIN— соединить, потомDISTINCTчтобы убрать дубликаты. Работает, но избыточно тащит лишние данные.IN (SELECT ...)— иногда работает, но проигрывает на больших данных и плохо обрабатывает NULL.EXISTS— спроектирован именно для «есть/нет», без лишних движений.Postgres умеет «остановиться» в подзапросе на первой найденной строке (
EXISTSназывают «short-circuit»). Это в разы быстрее, чем считать все связанные строки и потом схлопывать черезDISTINCT.Базовый синтаксис
SELECT * FROM таблица_A a WHERE EXISTS ( SELECT 1 FROM таблица_B b WHERE b.something = a.something );Логика: для каждой строки
aPostgres выполняет подзапрос. Если он вернул хотя бы одну строку —EXISTS = TRUE, строкаaпопадает в результат. Если ноль —FALSE, не попадает.SELECT 1внутри — это идиома. Что именно выбрать в подзапросе, неважно — Postgres всё равно проверяет только наличие, не содержимое.SELECT *,SELECT b.id,SELECT 'anything'— всё работает одинаково.Пример с таблицами
customers:orders:Запрос «клиенты, у которых есть хотя бы один заказ»:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Результат:
Анна (1) и Вера (3) есть в orders. Боб (2) и Гриша (4) — нет, отфильтровались.
Обрати внимание: Аня появилась один раз, хотя у неё два заказа.
EXISTSне дублирует — он бинарный (есть/нет), не считает количество.NOT EXISTS — обратная проверка
«Клиенты, у которых нет ни одного заказа» —
NOT EXISTS:SELECT * FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Результат: Боб и Гриша.
Это типичный «anti-join»: «дай мне строки из A, у которых нет соответствия в B». Часто используется для поиска осиротевших данных, неиспользуемых аккаунтов, забытых тегов.
EXISTS vs IN
Часто их сравнивают. На простых сценариях оба работают:
-- EXISTS WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id); -- IN WHERE c.id IN (SELECT customer_id FROM orders);Разница важна:
NOT INломается на NULLWHERE a=A AND b=B)WHERE (a, b) IN (SELECT ...))Особо опасно:
NOT IN (SELECT ...). Если в подзапросе хоть одна NULL — результат всегоNOT INстановитсяUNKNOWN(= не TRUE), и ничего не выбирается. Это очень тонкий баг, ловит почти всех.-- ОПАСНО: если в orders есть строка с customer_id = NULL — результат пустой SELECT * FROM customers WHERE id NOT IN (SELECT customer_id FROM orders); -- БЕЗОПАСНО: NOT EXISTS не страдает от NULL SELECT * FROM customers c WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);Правило простое: для anti-join всегда используй
NOT EXISTS, а неNOT IN.Условия из нескольких колонок
EXISTSхорошо работает на сложных условиях с несколькими полями:-- Клиенты, у которых хотя бы один платный заказ за последний месяц SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.status = 'paid' AND o.created_at > NOW() - INTERVAL '1 month' );Все условия внутри подзапроса. Снаружи — простое «есть/нет». Чисто и читаемо.
EXISTS в SELECT
Можно использовать
EXISTSне только вWHERE, но и как boolean-значение вSELECT:-- Каждому клиенту — флаг «есть ли у него заказы» SELECT c.id, c.name, EXISTS(SELECT 1 FROM orders o WHERE o.customer_id = c.id) AS has_orders FROM customers c;Получается дополнительная boolean-колонка. Удобно для отчётов «активные / неактивные».
Частые ошибки новичков
1.
NOT INс NULL в подзапросе. Главная ловушка. Уже разобрали — для anti-join всегда используйNOT EXISTS.2. Не понимают коррелированный подзапрос. Внутри
EXISTSобычно есть ссылка на внешнюю таблицу (o.customer_id = c.id). Это коррелированный подзапрос — он выполняется заново для каждой строки внешнего запроса. Postgres оптимизирует, но логически именно так.3.
SELECT *vsSELECT 1внутриEXISTS. Без разницы. Postgres даже не посмотрит на список колонок — он только проверяет наличие. Принято писатьSELECT 1для ясности.4. Делают
EXISTSбез коррелированной связи.WHERE EXISTS (SELECT 1 FROM orders)— без ссылки на внешнюю таблицу. Это вернётTRUEдля всех строк, если вordersвообще есть хоть одна запись. Скорее всего, не то что ты хотел.5.
JOIN + DISTINCTвместоEXISTS. Иногда работает, но избыточно.EXISTSчище и обычно быстрее, потому что не тащит данные.6. Не пишут
LIMIT 1(внутри EXISTS). Не нужно —EXISTSсам останавливается на первой строке.SELECT 1 FROM ... WHERE ... LIMIT 1ничего не ускорит, только запутает.Мини-резюме
EXISTS (SELECT 1 FROM ... WHERE ...)—TRUEесли подзапрос вернул хотя бы одну строку.NOT EXISTS— наоборот,TRUEесли ничего не нашлось. Для anti-join — единственно правильный способ.EXISTSпринято писатьSELECT 1— содержимое неважно.EXISTSобычно быстрее и чище, чемJOIN + DISTINCT.NOT IN (SELECT ...)— опасно, если в подзапросе могут быть NULL. ИспользуйNOT EXISTS.EXISTS.