SQLEXISTSsubquerytutorial

Что такое EXISTS в SQL?

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

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

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

Результат:

id name
1 Anna
3 Vera

Почему так?

У 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 два заказа, но в результате она появилась один раз:

id name
1 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
);

Результат:

id name
2 Bob
4 Gleb

У 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.

Ещё пример: посты с комментариями

Пусть есть таблицы:

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

Результат:

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;

Теперь результат станет правильным:

id name
1 Anna
3 Vera

Но запрос стал менее точным по смыслу.

Мы не хотим соединять клиентов с заказами.
Мы не хотим получать данные заказов.
Мы не хотим потом убирать дубликаты.

Мы хотим только проверить, что заказ существует.

Поэтому вариант с 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-способ сказать базе данных: «Покажи мне строки, для которых в другой таблице что-то нашлось».

Похожие статьи

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

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

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