SQLEXISTSsubquerytutorial

Что такое EXISTS в SQL? Проверка наличия для начинающих

EXISTS — это «есть ли хотя бы одна строка, удовлетворяющая условию». Простыми словами: фильтр на наличие связанной записи (например «клиенты, у которых есть хотя бы один заказ»), отличие от IN с подзапросом, NOT EXISTS и поведение с NULL.

4 мин чтенияСправочникSQL · EXISTS · subquery · tutorial

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
);

Результат:

id name
1 Аня
3 Вера

Анна (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);

Разница важна:

Критерий 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), и ничего не выбирается. Это очень тонкий баг, ловит почти всех.

-- ОПАСНО: если в 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 * 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.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр