Разберите, какой результат вернёт запрос и почему значение NULL внутри подзапроса не делает EXISTS ложным.
WITH candidates(id, marker) AS (
VALUES (1, NULL), (1, 'ok'), (2, NULL)
)
SELECT x.id,
EXISTS (
SELECT c.marker
FROM candidates AS c
WHERE c.id = x.id
) AS has_candidate
FROM (VALUES (1), (2), (3)) AS x(id);
EXISTS проверяет только наличие хотя бы одной строки, удовлетворяющей условию подзапроса. Он возвращает TRUE для id = 1 и id = 2, даже если найденное значение marker равно NULL; для id = 3 результатом будет FALSE.
В реляционных запросах часто требуется проверить факт существования связанной записи, не извлекая её значения. Предикат EXISTS предназначен именно для такого условия и отделяет проверку наличия строк от проверки содержимого их столбцов.
Это позволяет выразить семантику полусоединения: внешняя строка либо сохраняется один раз, если подходящая строка существует, либо отбрасывается. В отличие от обычного JOIN, совпадения внутри подзапроса не размножают строки внешнего результата.
В запросе подзапрос возвращает столбец marker, но EXISTS не сравнивает его с TRUE, NULL или каким-либо другим значением. Он смотрит только на количество строк результата.
Ошибочная замена EXISTS на проверку выбранного столбца может изменить смысл запроса: существующая строка с NULL будет ошибочно воспринята как отсутствующая. Другая распространённая ошибка — замена EXISTS на JOIN, которая может породить дубликаты внешних строк.
Для id = 1 подзапрос возвращает две строки, поэтому EXISTS даёт TRUE. Для id = 2 возвращается одна строка с marker = NULL, но строка всё равно существует, поэтому результат также TRUE. Для id = 3 подзапрос не возвращает строк, и результат — FALSE.
SELECT 1 внутри EXISTS — обычная идиоматическая запись: конкретное значение не имеет значения. Даже SELECT NULL дал бы тот же логический результат, если подзапрос возвращает те же строки.
Если подзапрос возвращает несколько совпадений, EXISTS всё равно возвращает только один логический результат для внешней строки. СУБД может остановить поиск после обнаружения первого совпадения, но полагаться на конкретный физический план не следует: важна только логика результата.
EXISTS также отличается от IN при наличии NULL в сравниваемых значениях. EXISTS оценивает явно заданное коррелированное условие, тогда как IN подчиняется трёхзначной логике SQL при сравнении с множеством, содержащим NULL.
В системе доступа нужно вывести пользователей, у которых есть хотя бы одно действующее разрешение. Вариант с JOIN может вернуть пользователя несколько раз, если разрешений несколько. DISTINCT устранит дубликаты, но добавит дополнительную операцию и может скрыть ошибку в логике запроса.
EXISTS точнее выражает требование наличия:
Преимущество — одна строка пользователя независимо от числа разрешений и возможность эффективного поиска первого совпадения по индексу, например по (user_id, active). Если же нужно вывести сами разрешения, их атрибуты или посчитать количество, EXISTS уже недостаточен: тогда нужен JOIN или агрегирование.
Вопрос: Может ли EXISTS вернуть NULL?
Ответ: Нет. Предикат EXISTS возвращает только TRUE или FALSE: TRUE, если подзапрос вернул хотя бы одну строку, и FALSE, если не вернул ни одной. Значения NULL в выбранных столбцах подзапроса на это не влияют.
Вопрос: Почему замена EXISTS на JOIN может изменить число строк?
Ответ: JOIN формирует строку для каждой пары совпавших строк. Если для одной внешней строки найдено три подходящие строки, результат соединения обычно содержит три строки. EXISTS проверяет сам факт совпадения и сохраняет внешнюю строку один раз.
Вопрос: Влияет ли ORDER BY внутри подзапроса EXISTS на его логический результат?
Ответ: Нет, порядок строк не меняет факт существования. Без ограничителя вроде LIMIT такой ORDER BY не добавляет смысловой информации к EXISTS и обычно должен быть удалён; если требуется выбрать конкретную строку или проверить её свойство, условие нужно выразить явно, например через агрегирование или отдельный коррелированный запрос.