Программирование SQLJOIN, подзапросы и CTEРазработчик серверной части

Какое значение возвращает коррелированный скалярный подзапрос для строки внешнего запроса, если он не наход...

Какое значение возвращает коррелированный скалярный подзапрос для строки внешнего запроса, если он не находит ни одной строки?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Коррелированный скалярный подзапрос, не вернувший ни одной строки, возвращает NULL. Строка внешнего запроса при этом не удаляется, если подзапрос используется как выражение, например в списке SELECT.

Это отличается от ситуации, когда подзапрос возвращает более одной строки: тогда возникает ошибка нарушения ожидаемой скалярности.

Исторический контекст

Подзапросы появились как способ выразить вложенную логику непосредственно внутри SQL-выражения, не разбивая её обязательно на отдельные запросы или временные таблицы. Скалярный подзапрос расширяет эту идею: результат поиска можно использовать как одно значение для каждой строки внешнего запроса.

Правило возврата NULL для пустого результата позволяет сохранить строку внешнего запроса и явно обозначить отсутствие связанного значения. Это особенно полезно для необязательных связанных данных.

Постановка проблемы

Рассмотрим отчёт, который должен вывести всех клиентов и сумму их единственного актуального платежа. У части клиентов платежа нет. Если неправильно понять поведение скалярного подзапроса, можно ошибочно ожидать удаления таких клиентов или получить неявное исключение строк фильтром.

Важно также помнить, что NULL означает не только отсутствие строки. Такое же значение получится, если найденная строка существует, но само выбранное поле содержит NULL. Без дополнительной проверки эти случаи неразличимы.

Подробное решение

Скалярный подзапрос должен вернуть максимум одну строку для каждой строки внешнего запроса. Если строк нет, результатом выражения становится NULL; если найдена ровно одна строка, возвращается значение выбранного столбца.

WITH clients(id) AS ( VALUES (1), (2) ), payments(client_id, status, amount) AS ( VALUES (1, 'paid', 100), (1, 'cancelled', 50) ) SELECT c.id, ( SELECT p.amount FROM payments p WHERE p.client_id = c.id AND p.status = 'paid' ) AS paid_amount FROM clients c;

Для клиента 1 подзапрос возвращает 100, а для клиента 2NULL. Обе строки клиентов остаются в результате, потому что подзапрос вычисляется как выражение, а не как условие отбора.

Если тот же результат используется в предикате, поведение меняется на уровне фильтрации. Сравнение NULL с обычным значением даёт состояние UNKNOWN, поэтому строка не проходит условие WHERE; это следствие трёхзначной логики SQL.

Оптимизатор может преобразовать коррелированный подзапрос во внутреннее соединение или другой эквивалентный план. Такое преобразование не должно менять наблюдаемую семантику: отсутствие результата подзапроса всё равно должно соответствовать NULL, а нарушение скалярности — сохранять корректное поведение согласно СУБД.

Для гарантии единственного результата нужна логическая гарантия: например, уникальность подходящей строки. Агрегат без GROUP BY тоже возвращает одну строку даже при пустом входе, но это уже другая семантика: MAX, SUM и подобные функции вычисляют агрегат, а не просто выбирают единственное найденное значение.

Ситуация из практики

В карточке клиента нужно показать его текущий тариф. Для каждого клиента бизнес-правило допускает не более одного текущего тарифа, а отсутствие тарифа является штатным состоянием.

Можно использовать коррелированный скалярный подзапрос. Он краток и непосредственно выражает правило «получить значение тарифа для этого клиента», но требует ограничения уникальности; без него несколько текущих тарифов приведут к ошибке.

Можно применить LEFT JOIN. Такой вариант хорошо показывает сохранение клиентов без тарифа, но при нарушении уникальности создаст несколько строк клиента. Дополнительная агрегация устранит дублирование, однако начнёт скрывать ошибку качества данных или произвольно выбирать значение.

Выбран скалярный подзапрос вместе с ограничением уникальности на текущий тариф. В результате клиент без тарифа получает NULL, клиент с тарифом — его идентификатор, а нарушение бизнес-правила обнаруживается явно, а не маскируется агрегацией.

Что кандидаты часто упускают

1. Можно ли по NULL определить, что подзапрос не нашёл строку?

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

2. Удаляет ли пустой скалярный подзапрос строку внешнего запроса?

Нет, если он находится в выражении SELECT, ORDER BY или другом контексте, где вычисляется значение. Внешняя строка сохраняется, а выражение получает NULL; удалить строку может уже внешний фильтр, использующий это значение в WHERE или HAVING.

3. Почему бездумная замена скалярного подзапроса на соединение опасна?

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