EXISTS обычно появляется в тот момент, когда обычный JOIN уже вроде понятен, но задача звучит немного иначе: "найди строки, для которых существует связанная запись"
Не "покажи все заказы клиентов", а "покажи клиентов, у которых есть хотя бы один оплаченный заказ". Не "соедини таблицы", а "проверь факт существования"
На словах разница кажется тонкой. На практике EXISTS очень удобен, когда нужно ответить да или нет: есть связанные строки или нет. В этом уроке разберем его на маленькой базе клиентов и заказов
- Что получится в конце
- Готовим учебные таблицы
- Первый EXISTS
- Как читать этот запрос
- EXISTS с дополнительным условием
- NOT EXISTS: ищем отсутствующие связи
- EXISTS или JOIN
- EXISTS или IN
- Типичная ошибка: забыли связать подзапрос с внешней таблицей
- Типичная ошибка: ждать от EXISTS данные
- Индексы и производительность простыми словами
- Мини-задания
- Ответы на эти вопросы могут быть для вас полезными
- Что делает EXISTS в SQL?
- Почему внутри EXISTS пишут SELECT 1?
- Чем EXISTS отличается от JOIN?
- Когда использовать NOT EXISTS?
- EXISTS быстрее IN?
- Что почитать дальше по SQL
Что получится в конце
Мы напишем запрос:
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
);
Ожидаемый результат:
| id | name |
|---|---|
| 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'
);
Ожидаемый результат:
| id | name |
|---|---|
| 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
);
Результат:
| id | name |
|---|---|
| 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 вернет клиента два раза:
| id | name |
|---|---|
| 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
Мини-задания
- Найди клиентов, у которых есть заказ со статусом
pending. - Найди клиентов, у которых нет отмененных заказов.
- Найди клиентов из Казани, у которых есть оплаченный заказ.
- Перепиши первый запрос через
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
Если вы собираете тему по шагам, рядом лучше открыть:
- JOIN в SQL: INNER, LEFT и ошибки новичка — понять, когда связь удобнее выразить через JOIN.
- SQL с нуля: первый запрос, фильтр и сортировка — освежить WHERE и базовую структуру SELECT.
- GROUP BY и HAVING: группировка без каши — перейти от проверки наличия строк к агрегатам.
- SQL-инъекция простыми словами и как защититься — увидеть, почему условия в запросах нельзя собирать небезопасно.



