«Скалярный подзапрос» звучит сложно, но на самом деле всё просто: это 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
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;
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 1 (с ORDER 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, COUNT — 0. Оборачивай в COALESCE если ожидаешь число.
«Скалярный подзапрос» звучит сложно, но на самом деле всё просто: это
SELECT, который возвращает ровно одно значение. Одна строка, одна колонка, одно число (или строка, или дата — что угодно «скалярное»).Такой подзапрос можно вставить прямо там, где обычно стоит колонка или константа: в
SELECT-список, вWHERE, вSETкоманды UPDATE. Postgres вычислит его и подставит результат.Зачем нужны скалярные подзапросы
Самый частый сценарий — дополнить выборку одной цифрой:
Альтернативы —
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:orders:Запрос «к каждому клиенту дописать сумму его заказов»:
SELECT c.id, c.name, (SELECT SUM(amount) FROM orders WHERE customer_id = c.id) AS total_spent FROM customers c;Результат:
Для каждого клиента 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 );Для каждой строки
customersPostgres выполнит подзапрос с подстановкойc.id, обновит колонку.Скалярный vs IN/EXISTS подзапросы
Принципиальная разница — что возвращается:
WHERE column IN (...)WHERE EXISTS (...)Если подзапрос вернул больше одной строки или колонки, а ты пытаешься использовать как скалярный — Postgres ругается:
Это самая частая ошибка с скалярными подзапросами.
Защита через 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 1(сORDER 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,COUNT—0. Оборачивай вCOALESCEесли ожидаешь число.