SQLsubqueryscalartutorial

Что такое скалярный подзапрос в SQL? Одно значение в SELECT для начинающих

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

4 мин чтенияСправочникSQL · subquery · scalar · tutorial

«Скалярный подзапрос» звучит сложно, но на самом деле всё просто: это SELECT, который возвращает ровно одно значение. Одна строка, одна колонка, одно число (или строка, или дата — что угодно «скалярное»).

Такой подзапрос можно вставить прямо там, где обычно стоит колонка или константа: в SELECT-список, в WHERE, в SET команды UPDATE. Postgres вычислит его и подставит результат.

Зачем нужны скалярные подзапросы

Самый частый сценарий — дополнить выборку одной цифрой:

  • К каждому клиенту дописать «общая сумма его заказов».
  • К каждому товару — «средняя оценка из reviews».
  • К каждому посту — «количество комментариев».

Альтернативы — JOIN + GROUP BY или CTE — иногда сложнее. Скалярный подзапрос — короткий и читаемый.

Второй сценарий — сравнение с константой, которая не известна заранее:

  • «заказы дороже среднего» — WHERE amount > (SELECT AVG(amount) FROM orders).
  • «пользователи, зарегистрированные после конкретного значения» — WHERE created_at > (SELECT created_at FROM milestones WHERE name = 'launch_v2').

Базовый синтаксис

SELECT
  column1,
  (SELECT column FROM other_table WHERE условие) AS computed_column
FROM table;

В круглых скобках — обычный SELECT, который вернёт одно значение. Postgres подставит это значение в строку результата.

Пример: дописать цифру к каждому клиенту

customers:

id name
1 Аня
2 Боб
3 Вера

orders:

id customer_id amount
1 1 100
2 1 250
3 2 80

Запрос «к каждому клиенту дописать сумму его заказов»:

SELECT
  c.id,
  c.name,
  (SELECT SUM(amount) FROM orders WHERE customer_id = c.id) AS total_spent
FROM customers c;

Результат:

id name total_spent
1 Аня 350
2 Боб 80
3 Вера NULL

Для каждого клиента Postgres выполнил подзапрос с подстановкой c.id, получил сумму. У Веры заказов нет — SUM вернул NULL. Если хочешь 0 вместо NULL — оборачивай в COALESCE(..., 0).

В WHERE

Скалярный подзапрос можно использовать как «константу-результат другого запроса»:

-- Заказы дороже среднего по всей таблице
SELECT *
FROM orders
WHERE amount > (SELECT AVG(amount) FROM orders);

Или:

-- Все юзеры, зарегистрированные после самого первого активного клиента
SELECT *
FROM users
WHERE created_at > (
  SELECT MIN(created_at) FROM users WHERE last_login_at IS NOT NULL
);

Postgres вычисляет подзапрос один раз, потом сравнивает каждую строку с этим значением.

В SET у UPDATE

-- Обновить колонку «общая сумма» из агрегации orders
UPDATE customers c
SET total_spent = (
  SELECT COALESCE(SUM(amount), 0) FROM orders WHERE customer_id = c.id
);

Для каждой строки customers Postgres выполнит подзапрос с подстановкой c.id, обновит колонку.

Скалярный vs IN/EXISTS подзапросы

Принципиальная разница — что возвращается:

Подзапрос Возвращает Где используется
Скалярный Одно значение На месте колонки/константы
IN (subquery) Список значений В WHERE column IN (...)
EXISTS Boolean (есть/нет) В WHERE EXISTS (...)

Если подзапрос вернул больше одной строки или колонки, а ты пытаешься использовать как скалярный — Postgres ругается:

ERROR: more than one row returned by a subquery used as an expression

Это самая частая ошибка с скалярными подзапросами.

Защита через LIMIT 1

Если подзапрос может вернуть несколько строк, и ты хочешь «любую первую» — добавь LIMIT 1 (с явным ORDER BY, чтобы был детерминизм):

SELECT
  c.id,
  c.name,
  (SELECT created_at FROM orders WHERE customer_id = c.id ORDER BY created_at DESC LIMIT 1) AS last_order_at
FROM customers c;

«Дата самого свежего заказа» — ORDER BY ... DESC LIMIT 1. Postgres гарантирует одно значение.

Для агрегатов это не нужно — SUM, COUNT, AVG, MAX, MIN всегда возвращают одно значение.

Производительность

Скалярные подзапросы выполняются для каждой строки внешней таблицы (если коррелированные). На миллионе клиентов — миллион подзапросов, что часто медленно.

Если запрос тормозит — обычно лучше переписать через LEFT JOIN с GROUP BY:

-- Скалярный — может тормозить
SELECT c.id, c.name, (SELECT SUM(amount) FROM orders WHERE customer_id = c.id) AS total
FROM customers c;

-- LEFT JOIN — обычно быстрее на больших данных
SELECT c.id, c.name, COALESCE(SUM(o.amount), 0) AS total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;

Postgres иногда сам делает такое преобразование, но не всегда. На больших отчётах — проверяй EXPLAIN.

Частые ошибки новичков

1. «more than one row returned» ошибка. Подзапрос вернул больше одного значения. Либо добавь LIMIT 1ORDER BY), либо смени логику (MAX/MIN/SUM-агрегат, или подзапрос становится IN/EXISTS).

2. Скалярный подзапрос в SELECT с агрегатом во внешнем запросе. Если внешний запрос — SELECT name, COUNT(...) FROM ... GROUP BY name, и в SELECT стоит скалярный подзапрос — он должен быть либо константой, либо ссылаться только на name (то, что в GROUP BY). Иначе ошибка.

3. Зависимость от NULL. SUM пустой выборки — NULL. COUNT пустой — 0. Если подзапрос потенциально пустой и ты ожидаешь число — оборачивай в COALESCE(..., 0).

4. Использование там, где нужен JOIN. Если нужно «к каждой строке X — её Y и ещё немного Y2 и Y3» — это уже не один скаляр, это JOIN. Не делай 3-5 скалярных подзапросов в одном SELECT — переходи на JOIN.

5. Не понимают коррелированность. В подзапросе WHERE customer_id = c.id ссылается на внешнюю таблицу. Это коррелированный подзапрос — выполняется для каждой строки. Без c.id подзапрос «один раз», независимый.

6. Подзапрос возвращает несколько колонок. SELECT (SELECT id, name FROM ...) FROM ... — нельзя. Один скаляр = одна колонка. Если нужны две — два подзапроса (или JOIN).

Мини-резюме

  • Скалярный подзапрос — SELECT, возвращающий ровно одно значение (одна строка, одна колонка).
  • Используется на месте колонки в SELECT, как «константа» в WHERE, как значение в SET у UPDATE.
  • Если возможно несколько строк — LIMIT 1 с ORDER BY для детерминизма.
  • Агрегаты (SUM, COUNT, AVG, MAX, MIN) — всегда возвращают одно значение, безопасны.
  • На больших данных коррелированные подзапросы медленнее LEFT JOIN + GROUP BY. Проверяй EXPLAIN.
  • SUM пустой выборки даёт NULL, COUNT0. Оборачивай в COALESCE если ожидаешь число.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр