EXISTS в SQL на простых примерах

EXISTS обычно появляется в тот момент, когда обычный JOIN уже вроде понятен, но задача звучит немного иначе: "найди строки, для которых существует связанная запись"

Не "покажи все заказы клиентов", а "покажи клиентов, у которых есть хотя бы один оплаченный заказ". Не "соедини таблицы", а "проверь факт существования"

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

Что получится в конце

Мы напишем запрос:

SELECT c.id, c.name
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
    AND o.status = 'paid'
);

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

Готовим учебные таблицы

Создадим клиентов:

CREATE TABLE customers (
  id INTEGER PRIMARY KEY,
  name TEXT,
  city TEXT
);

Добавим данные:

INSERT INTO customers (id, name, city) VALUES
(1, 'Анна', 'Казань'),
(2, 'Игорь', 'Москва'),
(3, 'Мария', 'Самара'),
(4, 'Олег', 'Екатеринбург');

Теперь таблица заказов:

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER,
  amount INTEGER,
  status TEXT
);

И несколько заказов:

INSERT INTO orders (id, customer_id, amount, status) VALUES
(1, 1, 7900, 'paid'),
(2, 1, 15000, 'paid'),
(3, 2, 4500, 'pending'),
(4, 3, 1200, 'cancelled');

У Анны есть два оплаченных заказа. У Игоря есть заказ, но он пока не оплачен. У Марии заказ отменен. У Олега заказов вообще нет

Первый EXISTS

Задача: найти клиентов, у которых есть хотя бы один заказ

SELECT c.id, c.name
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
);

Ожидаемый результат:

idname
1Анна
2Игорь
3Мария

Олег не попал в результат, потому что в orders нет ни одной строки с customer_id = 4

Как читать этот запрос

Внешний запрос идет по таблице customers:

SELECT c.id, c.name
FROM customers c

Для каждого клиента внутренняя часть проверяет, есть ли хотя бы одна подходящая строка в orders:

SELECT 1
FROM orders o
WHERE o.customer_id = c.id

Если такая строка есть, EXISTS возвращает true, и клиент попадает в результат. Если нет — не попадает

Внутри часто пишут SELECT 1, потому что нам не нужны сами поля из orders. Нам важен факт: существует строка или нет

EXISTS с дополнительным условием

Теперь уточним задачу: нужны клиенты, у которых есть оплаченный заказ

SELECT c.id, c.name
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
    AND o.status = 'paid'
);

Ожидаемый результат:

idname
1Анна

Почему только Анна? У Игоря заказ есть, но статус pending. У Марии заказ отменен. У Олега заказов нет

Вот здесь EXISTS становится очень читаемым: "покажи клиентов, для которых существует оплаченный заказ"

NOT EXISTS: ищем отсутствующие связи

Теперь обратная задача: найти клиентов без заказов

SELECT c.id, c.name
FROM customers c
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
);

Результат:

idname
4Олег

NOT EXISTS — один из самых удобных способов найти "дыры": клиенты без заказов, товары без продаж, статьи без тегов, пользователи без подписки

Еще пример: клиенты, у которых нет оплаченных заказов:

SELECT c.id, c.name
FROM customers c
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
    AND o.status = 'paid'
);

Тут в результат попадут Игорь, Мария и Олег. У них нет ни одного заказа со статусом paid

EXISTS или JOIN

Похожую задачу можно решить через JOIN:

SELECT c.id, c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'paid';

Но есть нюанс. Если у клиента два оплаченных заказа, JOIN вернет клиента два раза:

idname
1Анна
1Анна

Можно добавить DISTINCT:

SELECT DISTINCT c.id, c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'paid';

Но если нам нужен именно факт существования, EXISTS часто читается точнее. Он не умножает строки внешней таблицы, потому что не вытаскивает каждую найденную строку из подзапроса

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

  • нужен список связанных строк — думай про JOIN;
  • нужно проверить, есть ли связанные строки — думай про EXISTS;
  • нужно найти отсутствие связи — часто подходит NOT EXISTS.

EXISTS или IN

Можно написать так:

SELECT id, name
FROM customers
WHERE id IN (
  SELECT customer_id
  FROM orders
  WHERE status = 'paid'
);

Для маленькой учебной таблицы результат будет тем же. Но EXISTS лучше показывает связь между внешней и внутренней таблицей:

WHERE o.customer_id = c.id

В реальных проектах выбор между IN и EXISTS зависит от базы, индексов и плана выполнения. Для новичка важнее понять смысл. IN спрашивает: значение входит в набор? EXISTS спрашивает: есть ли строка, которая подходит под условия?

Типичная ошибка: забыли связать подзапрос с внешней таблицей

Вот сломанная логика:

SELECT c.id, c.name
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.status = 'paid'
);

Внутри нет условия:

o.customer_id = c.id

Что произойдет? Если в таблице orders есть хотя бы один оплаченный заказ вообще, EXISTS станет true для каждого клиента. В результат попадут все клиенты, даже те, у кого заказов нет

Это одна из самых неприятных ошибок: запрос выполняется, но отвечает не на тот вопрос

Типичная ошибка: ждать от EXISTS данные

Новичок иногда думает, что SELECT 1 внутри EXISTS как-то попадет в результат. Нет. Внутренний запрос нужен только для проверки

Если нужны данные заказа, используй JOIN:

SELECT c.name, o.amount, o.status
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'paid';

Если нужен только список клиентов, EXISTS будет чище:

SELECT c.name
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
    AND o.status = 'paid'
);

Индексы и производительность простыми словами

В учебных данных EXISTS работает мгновенно. В настоящей базе важен индекс на поле связи:

CREATE INDEX idx_orders_customer_id
ON orders (customer_id);

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

CREATE INDEX idx_orders_customer_status
ON orders (customer_id, status);

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

Мини-задания

  1. Найди клиентов, у которых есть заказ со статусом pending.
  2. Найди клиентов, у которых нет отмененных заказов.
  3. Найди клиентов из Казани, у которых есть оплаченный заказ.
  4. Перепиши первый запрос через JOIN и сравни результат.

Возможное решение третьего задания:

SELECT c.id, c.name
FROM customers c
WHERE c.city = 'Казань'
  AND EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.id
      AND o.status = 'paid'
  );

Ответы на эти вопросы могут быть для вас полезными

Что делает EXISTS в SQL?

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

Почему внутри EXISTS пишут SELECT 1?

Потому что нам нужен не набор данных, а факт существования. SELECT 1 читается как "верни что угодно, если строка найдена"

Чем EXISTS отличается от JOIN?

JOIN соединяет строки и может размножить результат, если связей несколько. EXISTS проверяет наличие связанной строки и оставляет внешнюю строку один раз

Когда использовать NOT EXISTS?

Когда нужно найти отсутствие связи: клиенты без заказов, товары без продаж, пользователи без активной подписки, статьи без опубликованной версии

EXISTS быстрее IN?

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

Что почитать дальше по SQL

Если вы собираете тему по шагам, рядом лучше открыть:

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x