EXISTS в SQL — это проверка на существование.
Она отвечает на простой вопрос:
Есть ли хотя бы одна строка, которая подходит под условие?
Если такая строка есть, EXISTS возвращает TRUE.
Если не нашлось ни одной строки, возвращает FALSE.
Чаще всего EXISTS используют внутри WHERE, когда нужно отфильтровать строки по принципу «у этой записи есть связанная запись в другой таблице».
Например:
- показать клиентов, у которых есть хотя бы один заказ;
- найти товары, у которых есть отзывы;
- выбрать посты, у которых есть комментарии;
- найти пользователей, у которых есть активная подписка;
- наоборот, найти клиентов, у которых нет ни одного заказа.
Главная мысль: EXISTS не считает строки и не достаёт данные из подзапроса. Ему важно только одно: нашлось что-нибудь или нет.
Простая аналогия
Представьте администратора в фитнес-клубе.
К нему приходит человек, и администратор проверяет:
Есть ли у этого клиента активный абонемент?
Администратору не нужно доставать все абонементы клиента, считать их, печатать историю оплат и раскладывать документы по датам. Ему достаточно найти хотя бы один активный абонемент.
Нашёлся — можно пройти.
Не нашёлся — нельзя.
Вот так работает EXISTS.
Он не спрашивает: «Сколько строк?»
Он спрашивает: «Есть хотя бы одна?»
Зачем нужен EXISTS
Допустим, у нас есть две таблицы:
customers — клиенты;
orders — заказы.
Нужно получить клиентов, у которых есть хотя бы один заказ.
Можно попробовать сделать через JOIN, но тогда клиент с десятью заказами появится в результате десять раз. Потом придётся добавлять DISTINCT, чтобы убрать дубликаты.
Можно попробовать через IN, но у него есть неприятные особенности, особенно в обратных проверках с NOT IN и NULL.
А можно использовать EXISTS, потому что он как раз создан для таких задач:
Покажи строки из одной таблицы, если в другой таблице существует связанная строка.
Это читается почти как обычная человеческая мысль.
Базовый синтаксис EXISTS
Общий шаблон выглядит так:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Разберём идею по шагам.
Внешний запрос идёт по таблице customers:
SELECT *
FROM customers c
Для каждого клиента проверяется условие:
WHERE EXISTS (...)
Внутри EXISTS находится подзапрос:
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
Он ищет заказы, у которых customer_id совпадает с id текущего клиента.
Если хотя бы один заказ найден, EXISTS возвращает TRUE, и клиент попадает в результат.
Если заказов нет, EXISTS возвращает FALSE, и клиент отфильтровывается.
Почему внутри пишут SELECT 1
Внутри EXISTS часто пишут:
SELECT 1
На первый взгляд это выглядит странно. Почему 1? Почему не id, не *, не нужные колонки?
Потому что для EXISTS неважно, что именно возвращает подзапрос. Ему важно только, вернул подзапрос хотя бы одну строку или нет.
Все эти варианты по смыслу подходят:
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
SELECT o.id
FROM orders o
WHERE o.customer_id = c.id
SELECT *
FROM orders o
WHERE o.customer_id = c.id
Но в реальных SQL-запросах чаще пишут SELECT 1. Это короткая и понятная идиома:
Мы не выбираем данные, мы просто проверяем существование строки.
Пример с клиентами и заказами
Пусть есть таблица customers:
| id |
name |
| 1 |
Anna |
| 2 |
Bob |
| 3 |
Vera |
| 4 |
Gleb |
И таблица 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
);
Результат:
Почему так?
У Anna есть заказы:
| id |
customer_id |
amount |
| 1 |
1 |
100 |
| 2 |
1 |
250 |
Значит, для неё EXISTS возвращает TRUE.
У Vera тоже есть заказ:
| id |
customer_id |
amount |
| 3 |
3 |
80 |
Она тоже попадает в результат.
А у Bob и Gleb заказов нет. Для них подзапрос не находит ни одной строки, поэтому EXISTS возвращает FALSE.
EXISTS не размножает строки
Очень важное отличие от JOIN: EXISTS не дублирует строки внешней таблицы.
У Anna два заказа, но в результате она появилась один раз:
Почему?
Потому что EXISTS не соединяет клиента со всеми его заказами. Он просто проверяет:
Есть хотя бы один заказ?
Как только ответ «да», этого достаточно.
Если у клиента один заказ — TRUE.
Если у клиента десять заказов — всё равно TRUE.
Если у клиента тысяча заказов — всё равно TRUE.
EXISTS — это проверка «есть или нет», а не подсчёт.
Как EXISTS работает логически
Логически запрос можно представить так:
For each customer:
Check if there is at least one matching order
If yes, keep this customer
If no, skip this customer
То есть база как будто берёт клиента за клиентом и для каждого задаёт вопрос:
Есть ли в orders строка, где orders.customer_id = customers.id?
На практике современные базы данных умеют оптимизировать такие запросы. Они не обязаны буквально выполнять подзапрос заново наивным способом для каждой строки. Оптимизатор может превратить это в более эффективный план, например в semi join.
Но для понимания новичку полезно держать в голове именно такую картину: EXISTS проверяет наличие связанных строк для каждой строки внешнего запроса.
Коррелированный подзапрос
В примере выше внутри подзапроса есть ссылка на внешнюю таблицу:
WHERE o.customer_id = c.id
Здесь o.customer_id — колонка из таблицы orders, а c.id — колонка из внешней таблицы customers.
Такой подзапрос называется коррелированным.
Коррелированный подзапрос — это подзапрос, который зависит от текущей строки внешнего запроса.
Пример:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Подзапрос не живёт сам по себе. Для каждого клиента значение c.id будет другим:
- для
Anna это 1;
- для
Bob это 2;
- для
Vera это 3;
- для
Gleb это 4.
Поэтому проверка каждый раз немного разная.
NOT EXISTS: обратная проверка
NOT EXISTS делает обратное.
Он отвечает на вопрос:
Не существует ли подходящей строки?
Если подзапрос ничего не нашёл, NOT EXISTS возвращает TRUE.
Например, найдём клиентов, у которых нет ни одного заказа:
SELECT *
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Результат:
У Bob нет заказов.
У Gleb нет заказов.
Значит, они попали в результат.
А Anna и Vera не попали, потому что для них связанные строки в orders существуют.
Где используют NOT EXISTS
NOT EXISTS часто используют для поиска отсутствующих связей.
Например:
- клиенты без заказов;
- товары без продаж;
- посты без комментариев;
- пользователи без активной подписки;
- категории, в которых нет товаров;
- заказы без платежей;
- записи, которые ещё не были обработаны.
Пример: найти товары, которые ни разу не продавались.
SELECT *
FROM products p
WHERE NOT EXISTS (
SELECT 1
FROM order_items oi
WHERE oi.product_id = p.id
);
Читается так:
Верни товары, для которых не существует ни одной строки в order_items.
Это один из самых чистых способов написать запрос «найди то, чего нет».
EXISTS с дополнительными условиями
Внутри 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 *
FROM posts p
WHERE EXISTS (
SELECT 1
FROM comments c
WHERE c.post_id = p.id
);
Если нужен только посты с одобренными комментариями:
SELECT *
FROM posts p
WHERE EXISTS (
SELECT 1
FROM comments c
WHERE c.post_id = p.id
AND c.status = 'approved'
);
Если нужны посты без комментариев:
SELECT *
FROM posts p
WHERE NOT EXISTS (
SELECT 1
FROM comments c
WHERE c.post_id = p.id
);
Получается очень выразительно: EXISTS и NOT EXISTS буквально читаются как «существует» и «не существует».
EXISTS в SELECT
EXISTS можно использовать не только в WHERE, но и в списке колонок SELECT.
Например, мы хотим вывести всех клиентов и рядом показать флаг: есть у клиента заказы или нет.
SELECT
c.id,
c.name,
EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
) AS has_orders
FROM customers c;
Результат:
| id |
name |
has_orders |
| 1 |
Anna |
true |
| 2 |
Bob |
false |
| 3 |
Vera |
true |
| 4 |
Gleb |
false |
Это удобно для отчётов, админок и API, когда нужно не отфильтровать строки, а добавить к каждой строке признак.
Например:
has_orders;
has_comments;
has_active_subscription;
has_unread_messages.
EXISTS vs JOIN
Иногда задачу с EXISTS можно решить через JOIN.
Например, клиенты с заказами:
SELECT c.*
FROM customers c
JOIN orders o ON o.customer_id = c.id;
Но здесь есть проблема: если у клиента несколько заказов, клиент повторится несколько раз.
Для нашей таблицы результат будет таким:
| id |
name |
| 1 |
Anna |
| 1 |
Anna |
| 3 |
Vera |
Anna появилась два раза, потому что у неё два заказа.
Можно добавить DISTINCT:
SELECT DISTINCT c.*
FROM customers c
JOIN orders o ON o.customer_id = c.id;
Теперь результат станет правильным:
Но запрос стал менее точным по смыслу.
Мы не хотим соединять клиентов с заказами.
Мы не хотим получать данные заказов.
Мы не хотим потом убирать дубликаты.
Мы хотим только проверить, что заказ существует.
Поэтому вариант с EXISTS читается лучше:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Простое правило:
- нужны данные из обеих таблиц — используйте
JOIN;
- нужно только проверить наличие связанной строки — часто лучше
EXISTS.
EXISTS vs IN
Ещё один похожий вариант — IN.
Клиентов с заказами можно найти так:
SELECT *
FROM customers
WHERE id IN (
SELECT customer_id
FROM orders
);
На простых примерах это работает похоже на EXISTS.
Запрос с EXISTS:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Запрос с IN:
SELECT *
FROM customers
WHERE id IN (
SELECT customer_id
FROM orders
);
Оба могут вернуть клиентов, у которых есть заказы.
Но между ними есть различия.
IN удобно читать, когда вы проверяете одно значение против списка:
WHERE status IN ('paid', 'shipped', 'cancelled')
А EXISTS удобнее, когда нужно проверить наличие строки по условию, особенно если условий несколько:
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
AND o.status = 'paid'
AND o.created_at >= DATE '2026-01-01'
)
Кроме того, EXISTS часто лучше выражает намерение:
Мне не нужен список значений. Мне нужно знать, существует ли подходящая строка.
Главная ловушка NOT IN
Особенно важно быть осторожным с NOT IN.
На первый взгляд запрос выглядит нормально:
SELECT *
FROM customers
WHERE id NOT IN (
SELECT customer_id
FROM orders
);
Кажется, он должен вернуть клиентов без заказов.
Но если в подзапросе окажется хотя бы один NULL, результат может стать неожиданным: запрос может не вернуть ничего.
Почему так происходит?
NULL в SQL означает неизвестное значение. А сравнение с неизвестным значением даёт не TRUE и не FALSE, а UNKNOWN.
Допустим, подзапрос вернул такой набор:
1
3
NULL
Условие для клиента с id = 2 превращается в смысл:
2 is not 1
2 is not 3
2 is not unknown
Первые две части понятны. А третья неизвестна. SQL не может честно сказать, что 2 точно не равен неизвестному значению. В итоге условие не становится TRUE.
Поэтому NOT IN с NULL в подзапросе — классическая ловушка.
Почему NOT EXISTS безопаснее
Для поиска строк без связей лучше использовать NOT EXISTS.
Так безопаснее:
SELECT *
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Здесь NULL в orders.customer_id не ломает всю проверку.
Почему?
Потому что подзапрос ищет конкретную связь:
WHERE o.customer_id = c.id
Если o.customer_id равен NULL, он просто не совпадёт с c.id.
Такая строка не станет подходящей, но и не испортит результат для всех клиентов.
Практическое правило:
Для задач вида «найди строки, у которых нет связанной записи» используйте NOT EXISTS, а не NOT IN.
EXISTS и условия по нескольким колонкам
EXISTS особенно удобен, когда связь определяется не одной колонкой, а несколькими.
Например, есть таблица subscriptions, где подписка привязана к пользователю и продукту:
| user_id |
product_id |
status |
| 1 |
10 |
active |
| 1 |
20 |
cancelled |
| 2 |
10 |
active |
Нужно найти пользователей, у которых есть активная подписка на конкретный продукт.
SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM subscriptions s
WHERE s.user_id = u.id
AND s.product_id = 10
AND s.status = 'active'
);
Внутри EXISTS можно спокойно описать все условия:
- совпадает пользователь;
- совпадает продукт;
- статус активный.
С EXISTS такие запросы читаются естественно: есть строка, которая удовлетворяет всем условиям, или нет.
EXISTS и производительность
EXISTS часто хорошо работает на больших данных, потому что базе не нужно находить все подходящие строки. Ей достаточно понять, что нашлась хотя бы одна.
Если у клиента тысяча заказов, EXISTS не обязан обрабатывать их все ради результата TRUE. Как только подходящая строка найдена, проверку можно считать успешной.
Именно поэтому EXISTS часто называют проверкой с short-circuit: можно остановиться на первом найденном совпадении.
Но важно понимать: скорость зависит не только от слова EXISTS, но и от индексов.
Например, для такого запроса:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
очень полезен индекс на колонке связи:
CREATE INDEX orders_customer_id_idx ON orders(customer_id);
Тогда базе проще быстро находить заказы конкретного клиента.
Если индекса нет, база может быть вынуждена просматривать намного больше строк.
Индексы для EXISTS
Хорошее правило:
Если внутри EXISTS вы ищете строки по колонке, на этой колонке часто нужен индекс.
Например:
WHERE o.customer_id = c.id
Полезен индекс:
CREATE INDEX orders_customer_id_idx ON orders(customer_id);
Если внутри есть дополнительный фильтр по статусу:
WHERE o.customer_id = c.id
AND o.status = 'paid'
может быть полезен составной индекс:
CREATE INDEX orders_customer_status_idx ON orders(customer_id, status);
Если часто ищете оплаченные заказы за период:
WHERE o.customer_id = c.id
AND o.status = 'paid'
AND o.created_at >= DATE '2026-01-01'
может пригодиться индекс, учитывающий эти поля:
CREATE INDEX orders_customer_status_created_idx
ON orders(customer_id, status, created_at);
Не нужно создавать индексы бездумно на всё подряд. Но важно понимать: EXISTS становится особенно сильным, когда база может быстро проверить наличие подходящей строки.
EXISTS без связи с внешней таблицей
Одна из частых ошибок — написать EXISTS, но забыть связь с внешней таблицей.
Например:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
);
Что делает этот запрос?
Он проверяет:
Есть ли в таблице orders хотя бы одна строка вообще?
Если в orders есть хотя бы один заказ, условие EXISTS будет TRUE для каждого клиента. В результате вернутся все клиенты.
Если таблица orders пустая, не вернётся никто.
Скорее всего, это не то, что вы хотели.
Обычно внутри EXISTS нужна связь с внешней строкой:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Сравните:
WHERE EXISTS (
SELECT 1
FROM orders o
)
Это проверка существования заказов вообще.
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
)
Это проверка существования заказов у конкретного клиента.
Разница огромная.
Нужно ли писать LIMIT 1 внутри EXISTS
Иногда новички пишут так:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
LIMIT 1
);
Обычно LIMIT 1 внутри EXISTS не нужен.
EXISTS сам по смыслу проверяет только наличие хотя бы одной строки. База данных и так понимает, что ей не нужно собирать все строки подзапроса ради результата.
Лучше писать проще:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Так запрос чище и понятнее.
EXISTS с UPDATE
EXISTS можно использовать не только в SELECT, но и в других командах, например в UPDATE.
Допустим, нужно пометить клиентов как активных, если у них есть оплаченный заказ.
UPDATE customers c
SET is_active = TRUE
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
AND o.status = 'paid'
);
Логика такая:
Обнови только тех клиентов, для которых существует оплаченный заказ.
Это удобный способ обновлять строки на основе связанных данных.
EXISTS с DELETE
EXISTS можно использовать и в DELETE.
Например, нужно удалить товары, которые есть во временном списке на удаление:
DELETE FROM products p
WHERE EXISTS (
SELECT 1
FROM products_to_delete d
WHERE d.product_id = p.id
);
Или наоборот, удалить записи из временной таблицы, для которых уже нет основного объекта:
DELETE FROM product_cache pc
WHERE NOT EXISTS (
SELECT 1
FROM products p
WHERE p.id = pc.product_id
);
Но с DELETE всегда нужно быть осторожным: сначала проверьте условие через SELECT.
Например:
SELECT *
FROM product_cache pc
WHERE NOT EXISTS (
SELECT 1
FROM products p
WHERE p.id = pc.product_id
);
И только потом выполняйте удаление.
Частые ошибки новичков
Ошибка 1. Использовать NOT IN вместо NOT EXISTS
Опасный вариант:
SELECT *
FROM customers
WHERE id NOT IN (
SELECT customer_id
FROM orders
);
Если в orders.customer_id есть NULL, результат может стать пустым или неожиданным.
Безопаснее:
SELECT *
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Для поиска отсутствующих связей почти всегда лучше NOT EXISTS.
Ошибка 2. Забыть связь с внешней таблицей
Плохо:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
);
Этот запрос не проверяет заказы конкретного клиента. Он проверяет, есть ли заказы вообще.
Правильно:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Ошибка 3. Думать, что SELECT внутри EXISTS важен
Новичок может переживать, что лучше написать:
SELECT o.id
или:
SELECT *
Но для EXISTS содержимое SELECT не важно. Важно только, вернул подзапрос строку или нет.
Поэтому принято писать:
SELECT 1
Это коротко и ясно.
Ошибка 4. Использовать JOIN и DISTINCT там, где нужен EXISTS
Такой запрос может работать:
SELECT DISTINCT c.*
FROM customers c
JOIN orders o ON o.customer_id = c.id;
Но если данные из orders не нужны, вариант с EXISTS обычно понятнее:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
JOIN соединяет данные.
EXISTS проверяет наличие.
Это разные намерения.
Ошибка 5. Пытаться считать строки через EXISTS
EXISTS не говорит, сколько строк найдено.
Если нужно просто узнать, есть ли заказы, используйте EXISTS:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Если нужно узнать количество заказов, нужен COUNT:
SELECT
c.id,
c.name,
COUNT(o.id) AS orders_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;
Не путайте задачи:
EXISTS — есть или нет;
COUNT — сколько именно.
Ошибка 6. Добавлять LIMIT 1 без необходимости
Так писать обычно не нужно:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
LIMIT 1
);
Лучше проще:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
EXISTS и так означает «достаточно первой найденной строки».
Ошибка 7. Не ставить индекс на колонку связи
Запрос может быть правильным, но медленным:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Если таблица orders большая, полезен индекс:
CREATE INDEX orders_customer_id_idx ON orders(customer_id);
Без индекса базе может быть тяжело быстро проверять наличие заказов для каждого клиента.
Ошибка 8. Не понимать, что EXISTS возвращает boolean
EXISTS возвращает логическое значение: TRUE или FALSE.
Поэтому его можно использовать как условие:
WHERE EXISTS (...)
И как вычисляемую колонку:
SELECT
c.id,
EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
) AS has_orders
FROM customers c;
Это не список заказов и не количество заказов. Это именно флаг: да или нет.
Как выбрать: EXISTS, JOIN, IN или COUNT
Короткая шпаргалка:
| Задача |
Что выбрать |
| Нужны клиенты, у которых есть заказы |
EXISTS |
| Нужны клиенты, у которых нет заказов |
NOT EXISTS |
| Нужны клиенты вместе с данными заказов |
JOIN |
| Нужно проверить значение в коротком списке |
IN |
| Нужно посчитать количество заказов |
COUNT |
| Нужно добавить флаг «есть заказы» |
EXISTS в SELECT |
| Нужно найти отсутствующие связи |
NOT EXISTS |
Примеры.
Клиенты с заказами:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Клиенты без заказов:
SELECT *
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Клиенты и их заказы:
SELECT
c.id,
c.name,
o.id AS order_id,
o.amount
FROM customers c
JOIN orders o ON o.customer_id = c.id;
Количество заказов по клиентам:
SELECT
c.id,
c.name,
COUNT(o.id) AS orders_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;
Как читать EXISTS-запрос
Возьмём запрос:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
AND o.status = 'paid'
);
Его можно прочитать так:
Верни клиентов, для которых существует хотя бы один заказ со статусом paid.
А такой запрос:
SELECT *
FROM products p
WHERE NOT EXISTS (
SELECT 1
FROM order_items oi
WHERE oi.product_id = p.id
);
читается так:
Верни товары, для которых не существует ни одной строки в позициях заказов.
Если вы можете перевести запрос с EXISTS на обычный язык, значит, вы понимаете его правильно.
Главное
EXISTS проверяет, вернул ли подзапрос хотя бы одну строку.
Самое важное:
EXISTS возвращает TRUE, если подзапрос нашёл хотя бы одну строку;
EXISTS возвращает FALSE, если подзапрос не нашёл ничего;
- внутри
EXISTS принято писать SELECT 1, потому что содержимое строки не важно;
- чаще всего
EXISTS используют в WHERE;
NOT EXISTS ищет строки, для которых связанная запись не найдена;
- для задач «нет связанной строки» обычно лучше использовать
NOT EXISTS, а не NOT IN;
EXISTS не дублирует строки внешней таблицы, в отличие от JOIN;
- если нужны данные из второй таблицы, используйте
JOIN;
- если нужно только проверить наличие строки, часто лучше
EXISTS;
- для хорошей производительности важны индексы на колонках связи;
- не забывайте связывать подзапрос с внешней таблицей, иначе проверка будет относиться ко всей таблице целиком.
Если совсем коротко: EXISTS — это SQL-способ сказать базе данных: «Покажи мне строки, для которых в другой таблице что-то нашлось».
EXISTSв SQL — это проверка на существование.Она отвечает на простой вопрос:
Если такая строка есть,
EXISTSвозвращаетTRUE.Если не нашлось ни одной строки, возвращает
FALSE.Чаще всего
EXISTSиспользуют внутриWHERE, когда нужно отфильтровать строки по принципу «у этой записи есть связанная запись в другой таблице».Например:
Главная мысль:
EXISTSне считает строки и не достаёт данные из подзапроса. Ему важно только одно: нашлось что-нибудь или нет.Простая аналогия
Представьте администратора в фитнес-клубе.
К нему приходит человек, и администратор проверяет:
Администратору не нужно доставать все абонементы клиента, считать их, печатать историю оплат и раскладывать документы по датам. Ему достаточно найти хотя бы один активный абонемент.
Нашёлся — можно пройти.
Не нашёлся — нельзя.
Вот так работает
EXISTS.Он не спрашивает: «Сколько строк?»
Он спрашивает: «Есть хотя бы одна?»
Зачем нужен EXISTS
Допустим, у нас есть две таблицы:
customers— клиенты;orders— заказы.Нужно получить клиентов, у которых есть хотя бы один заказ.
Можно попробовать сделать через
JOIN, но тогда клиент с десятью заказами появится в результате десять раз. Потом придётся добавлятьDISTINCT, чтобы убрать дубликаты.Можно попробовать через
IN, но у него есть неприятные особенности, особенно в обратных проверках сNOT INиNULL.А можно использовать
EXISTS, потому что он как раз создан для таких задач:Это читается почти как обычная человеческая мысль.
Базовый синтаксис EXISTS
Общий шаблон выглядит так:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Разберём идею по шагам.
Внешний запрос идёт по таблице
customers:SELECT * FROM customers cДля каждого клиента проверяется условие:
WHERE EXISTS (...)Внутри
EXISTSнаходится подзапрос:SELECT 1 FROM orders o WHERE o.customer_id = c.idОн ищет заказы, у которых
customer_idсовпадает сidтекущего клиента.Если хотя бы один заказ найден,
EXISTSвозвращаетTRUE, и клиент попадает в результат.Если заказов нет,
EXISTSвозвращаетFALSE, и клиент отфильтровывается.Почему внутри пишут SELECT 1
Внутри
EXISTSчасто пишут:SELECT 1На первый взгляд это выглядит странно. Почему
1? Почему неid, не*, не нужные колонки?Потому что для
EXISTSневажно, что именно возвращает подзапрос. Ему важно только, вернул подзапрос хотя бы одну строку или нет.Все эти варианты по смыслу подходят:
SELECT 1 FROM orders o WHERE o.customer_id = c.idSELECT o.id FROM orders o WHERE o.customer_id = c.idSELECT * FROM orders o WHERE o.customer_id = c.idНо в реальных SQL-запросах чаще пишут
SELECT 1. Это короткая и понятная идиома:Пример с клиентами и заказами
Пусть есть таблица
customers:И таблица
orders:Нужно найти клиентов, у которых есть хотя бы один заказ.
Пишем:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Результат:
Почему так?
У
Annaесть заказы:Значит, для неё
EXISTSвозвращаетTRUE.У
Veraтоже есть заказ:Она тоже попадает в результат.
А у
BobиGlebзаказов нет. Для них подзапрос не находит ни одной строки, поэтомуEXISTSвозвращаетFALSE.EXISTS не размножает строки
Очень важное отличие от
JOIN:EXISTSне дублирует строки внешней таблицы.У
Annaдва заказа, но в результате она появилась один раз:Почему?
Потому что
EXISTSне соединяет клиента со всеми его заказами. Он просто проверяет:Как только ответ «да», этого достаточно.
Если у клиента один заказ —
TRUE.Если у клиента десять заказов — всё равно
TRUE.Если у клиента тысяча заказов — всё равно
TRUE.EXISTS— это проверка «есть или нет», а не подсчёт.Как EXISTS работает логически
Логически запрос можно представить так:
То есть база как будто берёт клиента за клиентом и для каждого задаёт вопрос:
На практике современные базы данных умеют оптимизировать такие запросы. Они не обязаны буквально выполнять подзапрос заново наивным способом для каждой строки. Оптимизатор может превратить это в более эффективный план, например в semi join.
Но для понимания новичку полезно держать в голове именно такую картину:
EXISTSпроверяет наличие связанных строк для каждой строки внешнего запроса.Коррелированный подзапрос
В примере выше внутри подзапроса есть ссылка на внешнюю таблицу:
WHERE o.customer_id = c.idЗдесь
o.customer_id— колонка из таблицыorders, аc.id— колонка из внешней таблицыcustomers.Такой подзапрос называется коррелированным.
Коррелированный подзапрос — это подзапрос, который зависит от текущей строки внешнего запроса.
Пример:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Подзапрос не живёт сам по себе. Для каждого клиента значение
c.idбудет другим:Annaэто1;Bobэто2;Veraэто3;Glebэто4.Поэтому проверка каждый раз немного разная.
NOT EXISTS: обратная проверка
NOT EXISTSделает обратное.Он отвечает на вопрос:
Если подзапрос ничего не нашёл,
NOT EXISTSвозвращаетTRUE.Например, найдём клиентов, у которых нет ни одного заказа:
SELECT * FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Результат:
У
Bobнет заказов.У
Glebнет заказов.Значит, они попали в результат.
А
AnnaиVeraне попали, потому что для них связанные строки вordersсуществуют.Где используют NOT EXISTS
NOT EXISTSчасто используют для поиска отсутствующих связей.Например:
Пример: найти товары, которые ни разу не продавались.
SELECT * FROM products p WHERE NOT EXISTS ( SELECT 1 FROM order_items oi WHERE oi.product_id = p.id );Читается так:
Это один из самых чистых способов написать запрос «найди то, чего нет».
EXISTS с дополнительными условиями
Внутри
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.Ещё пример: посты с комментариями
Пусть есть таблицы:
posts;comments.Нужно найти посты, у которых есть хотя бы один комментарий.
SELECT * FROM posts p WHERE EXISTS ( SELECT 1 FROM comments c WHERE c.post_id = p.id );Если нужен только посты с одобренными комментариями:
SELECT * FROM posts p WHERE EXISTS ( SELECT 1 FROM comments c WHERE c.post_id = p.id AND c.status = 'approved' );Если нужны посты без комментариев:
SELECT * FROM posts p WHERE NOT EXISTS ( SELECT 1 FROM comments c WHERE c.post_id = p.id );Получается очень выразительно:
EXISTSиNOT EXISTSбуквально читаются как «существует» и «не существует».EXISTS в SELECT
EXISTSможно использовать не только вWHERE, но и в списке колонокSELECT.Например, мы хотим вывести всех клиентов и рядом показать флаг: есть у клиента заказы или нет.
SELECT c.id, c.name, EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id ) AS has_orders FROM customers c;Результат:
Это удобно для отчётов, админок и API, когда нужно не отфильтровать строки, а добавить к каждой строке признак.
Например:
has_orders;has_comments;has_active_subscription;has_unread_messages.EXISTS vs JOIN
Иногда задачу с
EXISTSможно решить черезJOIN.Например, клиенты с заказами:
SELECT c.* FROM customers c JOIN orders o ON o.customer_id = c.id;Но здесь есть проблема: если у клиента несколько заказов, клиент повторится несколько раз.
Для нашей таблицы результат будет таким:
Annaпоявилась два раза, потому что у неё два заказа.Можно добавить
DISTINCT:SELECT DISTINCT c.* FROM customers c JOIN orders o ON o.customer_id = c.id;Теперь результат станет правильным:
Но запрос стал менее точным по смыслу.
Мы не хотим соединять клиентов с заказами.
Мы не хотим получать данные заказов.
Мы не хотим потом убирать дубликаты.
Мы хотим только проверить, что заказ существует.
Поэтому вариант с
EXISTSчитается лучше:SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Простое правило:
JOIN;EXISTS.EXISTS vs IN
Ещё один похожий вариант —
IN.Клиентов с заказами можно найти так:
SELECT * FROM customers WHERE id IN ( SELECT customer_id FROM orders );На простых примерах это работает похоже на
EXISTS.Запрос с
EXISTS:SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Запрос с
IN:SELECT * FROM customers WHERE id IN ( SELECT customer_id FROM orders );Оба могут вернуть клиентов, у которых есть заказы.
Но между ними есть различия.
INудобно читать, когда вы проверяете одно значение против списка:WHERE status IN ('paid', 'shipped', 'cancelled')А
EXISTSудобнее, когда нужно проверить наличие строки по условию, особенно если условий несколько:WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.status = 'paid' AND o.created_at >= DATE '2026-01-01' )Кроме того,
EXISTSчасто лучше выражает намерение:Главная ловушка NOT IN
Особенно важно быть осторожным с
NOT IN.На первый взгляд запрос выглядит нормально:
SELECT * FROM customers WHERE id NOT IN ( SELECT customer_id FROM orders );Кажется, он должен вернуть клиентов без заказов.
Но если в подзапросе окажется хотя бы один
NULL, результат может стать неожиданным: запрос может не вернуть ничего.Почему так происходит?
NULLв SQL означает неизвестное значение. А сравнение с неизвестным значением даёт неTRUEи неFALSE, аUNKNOWN.Допустим, подзапрос вернул такой набор:
Условие для клиента с
id = 2превращается в смысл:Первые две части понятны. А третья неизвестна. SQL не может честно сказать, что
2точно не равен неизвестному значению. В итоге условие не становитсяTRUE.Поэтому
NOT INсNULLв подзапросе — классическая ловушка.Почему NOT EXISTS безопаснее
Для поиска строк без связей лучше использовать
NOT EXISTS.Так безопаснее:
SELECT * FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Здесь
NULLвorders.customer_idне ломает всю проверку.Почему?
Потому что подзапрос ищет конкретную связь:
WHERE o.customer_id = c.idЕсли
o.customer_idравенNULL, он просто не совпадёт сc.id.Такая строка не станет подходящей, но и не испортит результат для всех клиентов.
Практическое правило:
Для задач вида «найди строки, у которых нет связанной записи» используйте
NOT EXISTS, а неNOT IN.EXISTS и условия по нескольким колонкам
EXISTSособенно удобен, когда связь определяется не одной колонкой, а несколькими.Например, есть таблица
subscriptions, где подписка привязана к пользователю и продукту:Нужно найти пользователей, у которых есть активная подписка на конкретный продукт.
SELECT * FROM users u WHERE EXISTS ( SELECT 1 FROM subscriptions s WHERE s.user_id = u.id AND s.product_id = 10 AND s.status = 'active' );Внутри
EXISTSможно спокойно описать все условия:С
EXISTSтакие запросы читаются естественно: есть строка, которая удовлетворяет всем условиям, или нет.EXISTS и производительность
EXISTSчасто хорошо работает на больших данных, потому что базе не нужно находить все подходящие строки. Ей достаточно понять, что нашлась хотя бы одна.Если у клиента тысяча заказов,
EXISTSне обязан обрабатывать их все ради результатаTRUE. Как только подходящая строка найдена, проверку можно считать успешной.Именно поэтому
EXISTSчасто называют проверкой с short-circuit: можно остановиться на первом найденном совпадении.Но важно понимать: скорость зависит не только от слова
EXISTS, но и от индексов.Например, для такого запроса:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );очень полезен индекс на колонке связи:
CREATE INDEX orders_customer_id_idx ON orders(customer_id);Тогда базе проще быстро находить заказы конкретного клиента.
Если индекса нет, база может быть вынуждена просматривать намного больше строк.
Индексы для EXISTS
Хорошее правило:
Например:
WHERE o.customer_id = c.idПолезен индекс:
CREATE INDEX orders_customer_id_idx ON orders(customer_id);Если внутри есть дополнительный фильтр по статусу:
WHERE o.customer_id = c.id AND o.status = 'paid'может быть полезен составной индекс:
CREATE INDEX orders_customer_status_idx ON orders(customer_id, status);Если часто ищете оплаченные заказы за период:
WHERE o.customer_id = c.id AND o.status = 'paid' AND o.created_at >= DATE '2026-01-01'может пригодиться индекс, учитывающий эти поля:
CREATE INDEX orders_customer_status_created_idx ON orders(customer_id, status, created_at);Не нужно создавать индексы бездумно на всё подряд. Но важно понимать:
EXISTSстановится особенно сильным, когда база может быстро проверить наличие подходящей строки.EXISTS без связи с внешней таблицей
Одна из частых ошибок — написать
EXISTS, но забыть связь с внешней таблицей.Например:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o );Что делает этот запрос?
Он проверяет:
Если в
ordersесть хотя бы один заказ, условиеEXISTSбудетTRUEдля каждого клиента. В результате вернутся все клиенты.Если таблица
ordersпустая, не вернётся никто.Скорее всего, это не то, что вы хотели.
Обычно внутри
EXISTSнужна связь с внешней строкой:SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Сравните:
WHERE EXISTS ( SELECT 1 FROM orders o )Это проверка существования заказов вообще.
WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id )Это проверка существования заказов у конкретного клиента.
Разница огромная.
Нужно ли писать LIMIT 1 внутри EXISTS
Иногда новички пишут так:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id LIMIT 1 );Обычно
LIMIT 1внутриEXISTSне нужен.EXISTSсам по смыслу проверяет только наличие хотя бы одной строки. База данных и так понимает, что ей не нужно собирать все строки подзапроса ради результата.Лучше писать проще:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Так запрос чище и понятнее.
EXISTS с UPDATE
EXISTSможно использовать не только вSELECT, но и в других командах, например вUPDATE.Допустим, нужно пометить клиентов как активных, если у них есть оплаченный заказ.
UPDATE customers c SET is_active = TRUE WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.status = 'paid' );Логика такая:
Это удобный способ обновлять строки на основе связанных данных.
EXISTS с DELETE
EXISTSможно использовать и вDELETE.Например, нужно удалить товары, которые есть во временном списке на удаление:
DELETE FROM products p WHERE EXISTS ( SELECT 1 FROM products_to_delete d WHERE d.product_id = p.id );Или наоборот, удалить записи из временной таблицы, для которых уже нет основного объекта:
DELETE FROM product_cache pc WHERE NOT EXISTS ( SELECT 1 FROM products p WHERE p.id = pc.product_id );Но с
DELETEвсегда нужно быть осторожным: сначала проверьте условие черезSELECT.Например:
SELECT * FROM product_cache pc WHERE NOT EXISTS ( SELECT 1 FROM products p WHERE p.id = pc.product_id );И только потом выполняйте удаление.
Частые ошибки новичков
Ошибка 1. Использовать NOT IN вместо NOT EXISTS
Опасный вариант:
SELECT * FROM customers WHERE id NOT IN ( SELECT customer_id FROM orders );Если в
orders.customer_idестьNULL, результат может стать пустым или неожиданным.Безопаснее:
SELECT * FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Для поиска отсутствующих связей почти всегда лучше
NOT EXISTS.Ошибка 2. Забыть связь с внешней таблицей
Плохо:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o );Этот запрос не проверяет заказы конкретного клиента. Он проверяет, есть ли заказы вообще.
Правильно:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Ошибка 3. Думать, что SELECT внутри EXISTS важен
Новичок может переживать, что лучше написать:
SELECT o.idили:
SELECT *Но для
EXISTSсодержимоеSELECTне важно. Важно только, вернул подзапрос строку или нет.Поэтому принято писать:
SELECT 1Это коротко и ясно.
Ошибка 4. Использовать JOIN и DISTINCT там, где нужен EXISTS
Такой запрос может работать:
SELECT DISTINCT c.* FROM customers c JOIN orders o ON o.customer_id = c.id;Но если данные из
ordersне нужны, вариант сEXISTSобычно понятнее:SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );JOINсоединяет данные.EXISTSпроверяет наличие.Это разные намерения.
Ошибка 5. Пытаться считать строки через EXISTS
EXISTSне говорит, сколько строк найдено.Если нужно просто узнать, есть ли заказы, используйте
EXISTS:SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Если нужно узнать количество заказов, нужен
COUNT:SELECT c.id, c.name, COUNT(o.id) AS orders_count FROM customers c LEFT JOIN orders o ON o.customer_id = c.id GROUP BY c.id, c.name;Не путайте задачи:
EXISTS— есть или нет;COUNT— сколько именно.Ошибка 6. Добавлять LIMIT 1 без необходимости
Так писать обычно не нужно:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id LIMIT 1 );Лучше проще:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );EXISTSи так означает «достаточно первой найденной строки».Ошибка 7. Не ставить индекс на колонку связи
Запрос может быть правильным, но медленным:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Если таблица
ordersбольшая, полезен индекс:CREATE INDEX orders_customer_id_idx ON orders(customer_id);Без индекса базе может быть тяжело быстро проверять наличие заказов для каждого клиента.
Ошибка 8. Не понимать, что EXISTS возвращает boolean
EXISTSвозвращает логическое значение:TRUEилиFALSE.Поэтому его можно использовать как условие:
WHERE EXISTS (...)И как вычисляемую колонку:
SELECT c.id, EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id ) AS has_orders FROM customers c;Это не список заказов и не количество заказов. Это именно флаг: да или нет.
Как выбрать: EXISTS, JOIN, IN или COUNT
Короткая шпаргалка:
EXISTSNOT EXISTSJOININCOUNTEXISTSвSELECTNOT EXISTSПримеры.
Клиенты с заказами:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Клиенты без заказов:
SELECT * FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );Клиенты и их заказы:
SELECT c.id, c.name, o.id AS order_id, o.amount FROM customers c JOIN orders o ON o.customer_id = c.id;Количество заказов по клиентам:
SELECT c.id, c.name, COUNT(o.id) AS orders_count FROM customers c LEFT JOIN orders o ON o.customer_id = c.id GROUP BY c.id, c.name;Как читать EXISTS-запрос
Возьмём запрос:
SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.status = 'paid' );Его можно прочитать так:
А такой запрос:
SELECT * FROM products p WHERE NOT EXISTS ( SELECT 1 FROM order_items oi WHERE oi.product_id = p.id );читается так:
Если вы можете перевести запрос с
EXISTSна обычный язык, значит, вы понимаете его правильно.Главное
EXISTSпроверяет, вернул ли подзапрос хотя бы одну строку.Самое важное:
EXISTSвозвращаетTRUE, если подзапрос нашёл хотя бы одну строку;EXISTSвозвращаетFALSE, если подзапрос не нашёл ничего;EXISTSпринято писатьSELECT 1, потому что содержимое строки не важно;EXISTSиспользуют вWHERE;NOT EXISTSищет строки, для которых связанная запись не найдена;NOT EXISTS, а неNOT IN;EXISTSне дублирует строки внешней таблицы, в отличие отJOIN;JOIN;EXISTS;Если совсем коротко:
EXISTS— это SQL-способ сказать базе данных: «Покажи мне строки, для которых в другой таблице что-то нашлось».