Аналитика10 сентября 2026 г.

EXISTS и NOT EXISTS: как найти клиентов с заказами и без них без дублей

Автор
Ненашев А.
На чтение
7 мин
Дата обновления
10.09.2026
Старт:В любой момент
Срок обучения:53 часа
Курс SQL. Базы данных. ORACLE. MS SQL Server, MySQL
Самый полный курс обучения языку SQL и навыкам работы с базами данных. С первых простых шагов до полного погружения в написание высокоэффективных запросов на языке SQL, разработки баз данных и азов программирования! Отлаженная годами программа включает в себя все возможные методы работы с данными и объектами баз данных, типичные практические задачи, методы решения их и возможности комбинаций методов. Помимо этого, программа включает нестандартные ситуации и ньюансы, которые встречаются на практике. И, как бонус, вопросы и задачи к собеседованиям и ответы на них. Не требуется специальная предварительная подготовка. Достаточно просто уметь пользоваться компьютером. Последовательно, шаг за шагом, Вы освоите язык SQL, научитесь работать с базами данных на примере СУБД ORACLE. С первого занятия Вы получаете доступ к Вашей личной тестовой базе данных и необходимую для этого программу PL/SQL Developer. Вы сможете решать практические задачи и отрабатывать навыки прямо со своего домашнего компьютера. Сперва Вы освоите такой набор команд SQL, которые одинаково работают на любой СУБД (ORACLE, MS SQL Server, MySQL и т.д.), это называется ANSI стандарт. Знание этих азов позволяют работать с любыми базами данных. Помимо этих основных (общих для всех СУБД команд), разбирается весь самый нужный функционал популярных СУБД. Хотя уклон программы сделан в сторону работы с ORACLE, тем не менее Вы будете уметь пользоваться функционалом, выходящим за рамки ANSI, и в других СУБД. После прохождения курса Вы сможете решать практически любые задачи на SQL.
Подробнее
9 500 ₽12 000 ₽
#COURSE##INNER#

EXISTS и NOT EXISTS: как найти клиентов с заказами и без них без дублей

Когда нужен не список заказов, а два списка клиентов — у кого заказы есть и у кого их нет, — обычный JOIN часто уводит в сторону. Он соединяет строки, а значит, клиент с двумя заказами появится дважды, с тремя — трижды. Потом запрос начинают «лечить» через DISTINCT, но это уже исправление побочного эффекта, а не точное выражение задачи.

Для такой проверки удобнее мыслить не «соединить таблицы», а «проверить, существует ли хотя бы одна связанная строка». Именно это делают EXISTS и NOT EXISTS. Ниже разберем один узкий вопрос: как получить два чистых списка клиентов без размножения строк и как быстро проверить, что результат совпадает с ожиданием.

Почему эта задача не сводится к обычному JOIN

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

Из-за этого INNER JOIN дает лишние строки. Запрос ниже вернет по одной строке на каждый заказ, а не по одной строке на клиента:

SELECT c.client_id, c.client_name
FROM clients c
JOIN orders o ON o.client_id = c.client_id;

Если у клиента три заказа, он появится три раза. DISTINCT может убрать повторы в результате, но он не меняет смысл уже выполненного соединения. Для учебной задачи это важное различие: EXISTS выражает намерение проверить наличие записи, а JOIN выражает намерение получить пары совпавших строк.

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

Учебный пример. Среда выполнения: PostgreSQL. Возьмем две таблицы: список клиентов и список заказов. В данных специально есть клиенты с несколькими заказами, чтобы было видно, где JOIN раздувает результат.

WITH clients AS (
    SELECT *
    FROM (VALUES
        (1, 'Анна'),
        (2, 'Борис'),
        (3, 'Вера'),
        (4, 'Глеб'),
        (5, 'Дина')
    ) AS t(client_id, client_name)
),
orders AS (
    SELECT *
    FROM (VALUES
        (101, 1, DATE '2024-05-10'),
        (102, 1, DATE '2024-06-02'),
        (103, 3, DATE '2024-06-15'),
        (104, 3, DATE '2024-06-20'),
        (105, 5, DATE '2024-07-01')
    ) AS t(order_id, client_id, order_date)
)
SELECT *
FROM clients;

По этим данным ожидаемый ответ легко проверить вручную. У Анны есть два заказа, у Веры — два, у Дины — один. У Бориса и Глеба заказов нет. Значит, итог должен быть таким:

КлиентЕсть заказы
АннаДа
БорисНет
ВераДа
ГлебНет
ДинаДа

Это и будет наш контрольный ориентир: список клиентов с заказами должен содержать Анну, Веру и Дину ровно по одному разу. Список без заказов — Бориса и Глеба.

Как получить клиентов с заказами через EXISTS

Теперь формулируем задачу буквально: для каждой строки из clients нужно проверить, существует ли хотя бы одна строка в orders с тем же client_id. Такой запрос называют коррелированным подзапросом, потому что внутренняя проверка ссылается на текущую строку внешнего запроса.

WITH clients AS (
    SELECT *
    FROM (VALUES
        (1, 'Анна'),
        (2, 'Борис'),
        (3, 'Вера'),
        (4, 'Глеб'),
        (5, 'Дина')
    ) AS t(client_id, client_name)
),
orders AS (
    SELECT *
    FROM (VALUES
        (101, 1, DATE '2024-05-10'),
        (102, 1, DATE '2024-06-02'),
        (103, 3, DATE '2024-06-15'),
        (104, 3, DATE '2024-06-20'),
        (105, 5, DATE '2024-07-01')
    ) AS t(order_id, client_id, order_date)
)
SELECT c.client_id, c.client_name
FROM clients c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.client_id = c.client_id
)
ORDER BY c.client_id;

Результат будет таким: 1 Анна, 3 Вера, 5 Дина. Важно, что каждая строка внешней таблицы либо проходит фильтр, либо нет. Даже если у Анны два заказа, строка Анны во внешнем SELECT одна, поэтому и в выдаче она будет одна. В документации PostgreSQL как раз показан этот эффект: запрос с EXISTS похож на inner join по смыслу совпадения, но не размножает строки внешней таблицы по числу найденных совпадений.

Почему внутри подзапроса часто пишут SELECT 1, а не SELECT o.order_id? Потому что для EXISTS важен не набор возвращаемых значений, а сам факт того, что подзапрос вернул хотя бы одну строку. По той же причине такой подзапрос обычно выполняется лишь до момента, когда наличие строки уже установлено. Это полезно понимать, но не стоит превращать в обещание, что EXISTS всегда будет быстрее любого JOIN: производительность зависит и от данных, и от плана выполнения.

Как получить клиентов без заказов через NOT EXISTS

Вторая половина задачи строится симметрично. Если EXISTS отвечает на вопрос «нашлась ли связанная строка», то NOT EXISTS отвечает на вопрос «не нашлось ли ни одной связанной строки». Для списка клиентов без заказов это почти дословная формулировка задачи.

WITH clients AS (
    SELECT *
    FROM (VALUES
        (1, 'Анна'),
        (2, 'Борис'),
        (3, 'Вера'),
        (4, 'Глеб'),
        (5, 'Дина')
    ) AS t(client_id, client_name)
),
orders AS (
    SELECT *
    FROM (VALUES
        (101, 1, DATE '2024-05-10'),
        (102, 1, DATE '2024-06-02'),
        (103, 3, DATE '2024-06-15'),
        (104, 3, DATE '2024-06-20'),
        (105, 5, DATE '2024-07-01')
    ) AS t(order_id, client_id, order_date)
)
SELECT c.client_id, c.client_name
FROM clients c
WHERE NOT EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.client_id = c.client_id
)
ORDER BY c.client_id;

Здесь результатом будут 2 Борис и 4 Глеб. Такой запрос особенно удобен тем, что он прямо отражает смысл проверки отсутствия. Вам не нужно строить LEFT JOIN и потом отдельно проверять, какие столбцы оказались NULL. Такой вариант тоже бывает полезен, но он решает задачу через соединение, а не через проверку существования.

Отдельно стоит помнить о типичной ловушке с NOT IN. В документации PostgreSQL отмечено, что если в результате подзапроса для NOT IN встречается NULL, итог может стать не true, а null, и строка не пройдет фильтр. Для задач вида «нет связанной записи» NOT EXISTS обычно надежнее и понятнее, потому что он проверяет сам факт нахождения строки, а не сравнивает значение со списком, в котором могут оказаться пустые значения.

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

После написания запроса полезно не ограничиваться фразой «похоже, работает». Удобнее завести маленький контрольный список с ожидаемым ответом и сравнить ожидание с фактом. Это особенно помогает, когда позже в подзапрос добавляются условия по дате, статусу или типу заказа.

WITH clients AS (
    SELECT *
    FROM (VALUES
        (1, 'Анна'),
        (2, 'Борис'),
        (3, 'Вера'),
        (4, 'Глеб'),
        (5, 'Дина')
    ) AS t(client_id, client_name)
),
orders AS (
    SELECT *
    FROM (VALUES
        (101, 1, DATE '2024-05-10'),
        (102, 1, DATE '2024-06-02'),
        (103, 3, DATE '2024-06-15'),
        (104, 3, DATE '2024-06-20'),
        (105, 5, DATE '2024-07-01')
    ) AS t(order_id, client_id, order_date)
),
control_list AS (
    SELECT *
    FROM (VALUES
        (1, true),
        (2, false),
        (3, true),
        (4, false),
        (5, true)
    ) AS t(client_id, expected_has_orders)
)
SELECT
    cl.client_id,
    c.client_name,
    cl.expected_has_orders,
    EXISTS (
        SELECT 1
        FROM orders o
        WHERE o.client_id = cl.client_id
    ) AS actual_has_orders
FROM control_list cl
JOIN clients c ON c.client_id = cl.client_id
WHERE cl.expected_has_orders <> EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.client_id = cl.client_id
)
ORDER BY cl.client_id;

Если запрос не вернул ни одной строки, сверка пройдена: ожидание совпало с вычисленным результатом для всех пяти клиентов. Это уже проверяемый итог, а не интуитивная уверенность. В нашем учебном примере также легко проверить арифметику списков: клиентов всего 5, с заказами 3, без заказов 2, и 3 + 2 = 5 без пересечений.

Что чаще всего ломает такую задачу:

  • использование JOIN вместо проверки существования, когда нужен только факт наличия;
  • добавление фильтра по заказам не внутрь EXISTS, а во внешний WHERE, из-за чего меняется смысл проверки;
  • замена NOT EXISTS на NOT IN без учета возможных NULL в подзапросе.

Практический вывод простой: если вы хотите получить два чистых списка клиентов, думайте от внешней таблицы clients и задавайте вопрос к каждой ее строке: есть ли хотя бы один связанный заказ или нет. Тогда запрос остается коротким, не плодит дубли и легче проверяется контрольным списком.

Где это отработать дальше

Во внутренней карточке каталога курса SQL от Prime Soft подтверждены отдельные темы по JOIN, работе с дублями и DISTINCT, подзапросам в WHERE с IN и EXISTS, а также по плану выполнения запросов. Это полезная связка именно для задач, где нужно выбрать между соединением таблиц и проверкой факта существования записи.

Курсы

103087
0,0
0 отзывов
Курс SQL. Базы данных. ORACLE. MS SQL Server, MySQL
  • В любой момент
  • 53 часа
  • с преподавателем
  • сертификат
9 500 ₽12 000 ₽
-21%
Перейти на сайт