Learning Platform
Урок 08.01 · 18 мин
Средний
SubqueryScalar subqueryINNOT INRow subquery

Что такое подзапрос

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

Откуда у этого инструмента три имени, которые нужно различать:

  • скалярный подзапрос — возвращает ровно один столбец и ровно одну строку. С точки зрения внешнего запроса — обычное значение, неотличимое от литерала.
  • многострочный подзапрос — один столбец, много строк. Это набор значений; в SQL он работает как операнд для IN, NOT IN, ANY, ALL.
  • табличный подзапрос — много столбцов и много строк. Используется во FROM (там его обязательно надо именовать через AS) и в EXISTS.

В этом уроке разберём первые два. Табличные подзапросы во FROM и EXISTS уйдут в следующий урок про коррелированные подзапросы.

Три формы подзапроса

Подзапрос — это SELECT в скобках. Различаются они по тому, что именно возвращают: одно значение, одну колонку или целую таблицу.

scalar1 строка x 1 столбец
пример(SELECT MAX(price) FROM products)
используется каклитерал
row / columnN строк x 1 столбец
пример(SELECT id FROM customers WHERE country = 'RU')
используется какоперанд IN / ANY / ALL
tableN строк x M столбцов
пример(SELECT id, country FROM customers)
используется кактаблица в FROM / EXISTS

Скалярный подзапрос

Контракт скаляра: ровно одна строка, ровно один столбец. Если ты соблюдаешь его, PostgreSQL разрешает писать подзапрос там же, где разрешён обычный литерал — в SELECT-списке, в WHERE-предикате, в ORDER BY, в CASE.

Классический сценарий — «средняя цена для сравнения»:

К каждому товару присоединим среднюю цену по всему каталогу — одним запросом, без JOIN

PostgreSQL

Что здесь происходит. Скаляр (SELECT AVG(price_cents)::INT FROM products) вычисляется один раз (планировщик его материализует как InitPlan), и подставляется в каждую строку как обычная константа. По сути, это удобный способ занести агрегат во внешний запрос, не нарушая GROUP BY-правила.

В WHERE это работает так же:

Товары дороже средней цены каталога — скаляр в роли границы

PostgreSQL

Эквивалент через JOIN потребовал бы сначала вычислить агрегат в подзапросе, потом джойнить — длиннее и менее читаемо. Скаляр сжимает это в одну строку.

Что если скаляр вернул больше одной строки

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

Этот запрос сломается. Прочитай ошибку — она важная.

PostgreSQL

Запусти — увидишь ERROR: more than one row returned by a subquery used as an expression. PostgreSQL отказывается «угадывать», какую из строк подставить. Это не warning, не fallback — это ошибка во время выполнения.

Правильные способы починить:

  1. Гарантировать одну строку через LIMIT 1 (плюс понятный ORDER BY, иначе результат недетерминирован).
  2. Гарантировать одну строку через агрегат (MAX, MIN, SUM — они всегда вернут ровно одну строку).
  3. Переписать через JOIN, если на самом деле нужны все «совпадения».

Чиним: агрегат гарантирует одну строку

PostgreSQL

Тонкость: если подзапрос вернул ноль строк, скаляр становится NULL. Это не ошибка — это легальный результат. Поэтому всегда обрабатывай NULL в местах, где используешь скаляр.

Многострочный подзапрос — IN, NOT IN, ANY, ALL

Когда подзапрос возвращает много строк одного столбца, его нельзя сравнить через =. Зато можно через операторы множества.

IN — «значение слева совпадает хотя бы с одним из значений справа».

Все заказы клиентов из России — без JOIN, через IN + подзапрос

PostgreSQL

IN с подзапросом — это синтаксический сахар над ANY: x IN (SELECT ...) эквивалентно x = ANY (SELECT ...). А x > ANY (SELECT y FROM ...) означает «x больше хотя бы одного y». ALL — наоборот, «больше каждого».

ANY и ALL — полезные братья IN

PostgreSQL

> ALL (SELECT ...) ровно эквивалентно > (SELECT MAX(...)). Оптимизатор это знает и часто переписывает форму в другую. Но писать через MAX обычно понятнее.

NULL-ловушка в NOT IN

Это — второе классическое место, где новички теряются. Прочитай внимательно, потому что ошибка тут не падает с экраном — она тихо съедает строки.

Найдём клиентов, у которых birth_year не входит в маленький набор. Запусти и посчитай результат:

PostgreSQL

Если в подзапросе появляется NULL (а в customers.birth_year он есть), NOT IN начинает вести себя катастрофически:

x NOT IN (a, b, NULL) эквивалентно x != a AND x != b AND x != NULL. Последнее — это NULL. По правилам трёхзначной логики TRUE AND NULL = NULL, и общий предикат становится NULL — значит, строка отсекается.

В итоге NOT IN с подзапросом, в котором может появиться NULL, возвращает либо пустой результат, либо часть строк, в зависимости от данных. Это всегда баг.

Лечение: либо явно отфильтровать NULL внутри подзапроса (WHERE col IS NOT NULL), либо использовать NOT EXISTS — он работает корректно с NULL. Подробно про EXISTS — в следующем уроке.

Чиним NOT IN — гасим NULL до того, как он попадёт в подзапрос

PostgreSQL

Row-subquery — несколько колонок одной строки

PostgreSQL поддерживает редкую, но красивую форму: сравнение row-конструктора со скалярным подзапросом, возвращающим много колонок одной строки.

Найди клиента с email и страной, совпадающими с конкретным шаблоном из подзапроса

PostgreSQL

Здесь подзапрос возвращает одну строку из двух колонок, и мы сравниваем его с парой (email, country). Это и есть row subquery в стандарте. Используется редко, но удобно, когда нужно проверить совпадение по композитному ключу.

Проверка знанийKnowledge check
У тебя есть запрос SELECT name FROM products WHERE price_cents > (SELECT price_cents FROM products WHERE sku = 'LP-001'). Что произойдёт, если sku не найден? А если найден дважды (например, ограничение UNIQUE на sku временно отключено)?
ОтветAnswer
Если sku не найден — подзапрос вернёт ноль строк, и скаляр станет NULL. Сравнение price_cents > NULL даёт NULL для каждой строки, и весь WHERE отсекает все строки — результат пустой. Это не ошибка, а тихий сюрприз. Если sku найден дважды — подзапрос вернёт две строки, и PostgreSQL упадёт с ошибкой 'more than one row returned by a subquery used as an expression'. Защита: либо MAX/MIN-агрегат, либо LIMIT 1 с детерминированным ORDER BY.
Как подзапросы попадают в физический план исполнения

Чек-лист

  • Подзапрос — это SELECT в скобках, по форме результата делится на скалярный, многострочный и табличный.
  • Скаляр обязан вернуть ровно одну строку из одного столбца. Ноль строк — NULL. Больше одной — runtime error.
  • Многострочный подзапрос работает с IN, NOT IN, ANY, ALL. IN — сахар над = ANY.
  • NOT IN ломается, если в подзапросе может быть NULL — выкидывает строки молча. Лечится IS NOT NULL или переписыванием через NOT EXISTS.
  • Row subquery (a, b) = (SELECT x, y FROM ...) — стандартный, но редко встречающийся способ сравнить композитный ключ за один шаг.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Скалярный подзапрос вернул ноль строк. Что произойдёт при попытке использовать его в выражении SELECT col * (subquery) FROM t?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6