Подзапросы: скалярный и в WHERE
Содержание курса
На предыдущих уроках вы соединяли таблицы через JOIN, чтобы добраться до данных из нескольких источников. Но бывает задача другого рода: нужно не вывести столбцы из второй таблицы, а использовать вычисленное из неё значение как условие фильтра. Именно для этого существуют подзапросы — запросы внутри запросов.
Скалярный подзапрос: одно значение как аргумент WHERE
Подзапрос — это обычный SELECT, обёрнутый в скобки и помещённый внутрь другого запроса. Скалярный подзапрос — частный случай: он возвращает ровно одно значение, и это значение используется там, где обычно стоит число или строка.
Самый наглядный пример — найти товар с максимальной ценой:
SELECT title
FROM products
WHERE price = (SELECT MAX(price) FROM products);
Внутренний SELECT MAX(price) FROM products — это и есть скалярный подзапрос. MAX без GROUP BY всегда возвращает один столбец и одну строку: если таблица непустая — максимум, если пустая — NULL. Именно агрегат без GROUP BY служит надёжным способом получить гарантированно одно значение.
Внешний запрос получает это число и сравнивает с ним каждую строку products. Строк в результате может быть несколько, если у нескольких товаров одинаковая максимальная цена — это нормально, ведь ограничение «одна строка» относится к подзапросу, а не к внешнему результату.
Что происходит, когда подзапрос возвращает NULL. Если таблица products пустая, MAX(price) вернёт NULL. Сравнение price = NULL в SQL всегда даёт UNKNOWN, а не TRUE, поэтому ни одна строка не пройдёт фильтр — внешний запрос вернёт пустой результат. Это логично, но нужно держать в голове.
Что происходит при нуле строк. Возьмём подзапрос, который действительно не возвращает строк:
SELECT title
FROM products
WHERE price = (SELECT price FROM products WHERE 1 = 0);
В скалярном контексте отсутствие строк превращается в NULL, поэтому сравнение не пропустит ни одного товара. Это отличается от MAX на пустом входе: агрегат без GROUP BY возвращает одну строку, но значение в ней тоже NULL.
Что происходит при нескольких строках. Скалярный подзапрос предполагает одну строку. Если написать что-то вроде WHERE price = (SELECT price FROM products) без агрегации, подзапрос вернёт много строк. В PostgreSQL это немедленная ошибка: more than one row returned by a subquery. SQLite поведёт себя мягче — возьмёт значение из первой строки и проигнорирует остальные, — но полагаться на это нельзя: запрос молча даст неверный результат на других СУБД. Агрегат снимает проблему именно потому, что схлопывает любое множество строк в одну.
Скалярный подзапрос можно поставить не только в WHERE, но и в SELECT (как вычисляемый столбец) или HAVING. Сейчас нас интересует WHERE — именно там он чаще всего решает задачу «сравнить строку с вычисленным эталоном».

