Скалярный подзапрос звучит как что-то из высшей математики, но в SQL всё гораздо спокойнее.
Скалярный подзапрос — это обычный SELECT, который возвращает ровно одно значение.
То есть результат должен быть таким:
- одна колонка;
- одна строка;
- одно конкретное значение: число, дата, строка,
NULL — что угодно.
Например:
SELECT AVG(amount)
FROM orders;
Если этот запрос возвращает одно значение, его можно вставить внутрь другого запроса туда, где обычно стоит обычное значение: число, дата, строка или колонка.
Представь, что SQL-запрос — это анкета. В одну ячейку анкеты можно вписать число вручную, а можно сказать: «Сначала посчитай вот это, а потом подставь результат сюда». Вот это и делает скалярный подзапрос.
Где можно использовать скалярный подзапрос
Скалярный подзапрос можно поставить почти в любое место, где SQL ждёт одно значение.
Чаще всего его используют:
- в
SELECT, чтобы добавить к строке вычисленное значение;
- в
WHERE, чтобы сравнить строку с результатом другого запроса;
- в
UPDATE, чтобы обновить колонку значением из другого запроса.
Например:
SELECT
id,
name,
(SELECT AVG(amount) FROM orders) AS avg_order_amount
FROM customers;
Внутренний запрос считает среднюю сумму заказа. Наружный запрос показывает клиентов и рядом с каждым клиентом выводит это среднее значение.
Важно: подзапрос внутри круглых скобок должен вернуть одно значение. Не таблицу, не список, не две колонки — именно одно значение.
Зачем нужны скалярные подзапросы
Скалярные подзапросы удобны, когда нужно аккуратно «дописать» к результату одну дополнительную цифру или сравнить данные с вычисленным значением.
Например:
- к каждому клиенту добавить общую сумму его заказов;
- к каждому товару добавить среднюю оценку;
- к каждому посту добавить количество комментариев;
- найти заказы дороже среднего;
- выбрать пользователей, зарегистрированных после важной даты из другой таблицы.
Да, часто то же самое можно сделать через JOIN, GROUP BY или CTE. Но для начинающего скалярный подзапрос иногда читается проще: «вот основная строка, а вот маленький запрос, который для неё что-то считает».
Базовый синтаксис
Общая форма выглядит так:
SELECT
column1,
column2,
(
SELECT some_column
FROM other_table
WHERE some_condition
) AS computed_column
FROM main_table;
В круглых скобках находится подзапрос. SQL выполняет его, получает одно значение и подставляет это значение в результат.
Главное правило:
один скалярный подзапрос = одна колонка и максимум одна строка.
Если подзапрос вернёт несколько строк, база данных не сможет понять, какое именно значение подставлять.
Пример: добавить сумму заказов к каждому клиенту
Допустим, есть таблица клиентов.
| id |
name |
| 1 |
Аня |
| 2 |
Боб |
| 3 |
Вера |
И есть таблица заказов.
| id |
customer_id |
amount |
| 1 |
1 |
100 |
| 2 |
1 |
250 |
| 3 |
2 |
80 |
Хочется получить список клиентов и рядом с каждым показать, сколько денег он потратил.
Запрос:
SELECT
c.id,
c.name,
(
SELECT SUM(o.amount)
FROM orders o
WHERE o.customer_id = c.id
) AS total_spent
FROM customers c;
Результат:
| id |
name |
total_spent |
| 1 |
Аня |
350 |
| 2 |
Боб |
80 |
| 3 |
Вера |
NULL |
Что здесь происходит?
Наружный запрос идёт по таблице customers. Для каждой строки он видит клиента: Аню, Боба, Веру.
А внутри для каждого клиента запускается маленький запрос:
SELECT SUM(o.amount)
FROM orders o
WHERE o.customer_id = c.id;
Для Ани c.id равен 1, значит подзапрос ищет заказы с customer_id = 1 и получает 350.
Для Боба c.id равен 2, получается 80.
Для Веры c.id равен 3, заказов нет. Поэтому SUM возвращает NULL.
Если вместо NULL хочется видеть 0, используй COALESCE:
SELECT
c.id,
c.name,
COALESCE((
SELECT SUM(o.amount)
FROM orders o
WHERE o.customer_id = c.id
), 0) AS total_spent
FROM customers c;
Теперь у клиента без заказов будет не NULL, а 0.
Коррелированный и некоррелированный подзапрос
Здесь важно понять одну вещь.
Подзапрос может быть независимым или связанным с наружным запросом.
Вот независимый подзапрос:
SELECT *
FROM orders
WHERE amount > (
SELECT AVG(amount)
FROM orders
);
Внутренний запрос не зависит от текущей строки наружного запроса. Он просто считает среднюю сумму заказа по всей таблице.
Такой подзапрос можно выполнить один раз, получить значение и потом сравнивать с ним каждую строку.
А вот связанный подзапрос:
SELECT
c.id,
c.name,
(
SELECT SUM(o.amount)
FROM orders o
WHERE o.customer_id = c.id
) AS total_spent
FROM customers c;
Внутри есть ссылка на c.id из наружного запроса. Значит, для каждого клиента значение будет своё.
Такой подзапрос называют коррелированным. Он как будто привязан ниточкой к каждой строке внешней таблицы.
Скалярный подзапрос в WHERE
Один из самых понятных сценариев — сравнить строки с каким-то вычисленным значением.
Например, найти заказы дороже среднего:
SELECT
id,
customer_id,
amount
FROM orders
WHERE amount > (
SELECT AVG(amount)
FROM orders
);
Внутренний запрос считает средний чек. Наружный запрос оставляет только те заказы, где amount больше этого среднего значения.
Другой пример: найти пользователей, которые зарегистрировались после даты запуска новой версии продукта.
SELECT
id,
name,
created_at
FROM users
WHERE created_at > (
SELECT created_at
FROM milestones
WHERE name = 'launch_v2'
);
Здесь подзапрос должен вернуть одну дату. Наружный запрос сравнивает с ней дату регистрации каждого пользователя.
Но будь внимателен: если в таблице milestones окажется две строки с name = 'launch_v2', запрос упадёт с ошибкой. SQL ожидал одну дату, а получил несколько.
Скалярный подзапрос в UPDATE
Скалярный подзапрос можно использовать и при обновлении данных.
Например, в таблице customers есть колонка total_spent, куда нужно записать сумму заказов каждого клиента.
UPDATE customers c
SET total_spent = COALESCE((
SELECT SUM(o.amount)
FROM orders o
WHERE o.customer_id = c.id
), 0);
Для каждой строки из customers база выполнит подзапрос, найдёт заказы этого клиента, посчитает сумму и запишет результат в total_spent.
Если заказов нет, SUM вернёт NULL, а COALESCE заменит его на 0.
Чем скалярный подзапрос отличается от IN и EXISTS
Подзапросы бывают разными. Главное отличие — что именно они возвращают.
| Вид подзапроса |
Что возвращает |
Где обычно используется |
| Скалярный подзапрос |
Одно значение |
В SELECT, WHERE, SET |
Подзапрос с IN |
Список значений |
В условии WHERE column IN (...) |
Подзапрос с EXISTS |
Ответ «есть строки или нет» |
В условии WHERE EXISTS (...) |
Скалярный подзапрос нужен, когда тебе нужно одно значение.
Например:
SELECT *
FROM orders
WHERE amount > (
SELECT AVG(amount)
FROM orders
);
А IN нужен, когда значений может быть много:
SELECT *
FROM orders
WHERE customer_id IN (
SELECT id
FROM customers
WHERE city = 'Moscow'
);
А EXISTS нужен, когда важен сам факт существования строк:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
То есть вопрос не в том, «какой подзапрос лучше», а в том, какую задачу ты решаешь.
Если нужно одно значение — скалярный подзапрос.
Если нужен список — IN.
Если нужно проверить наличие — EXISTS.
Ошибка: подзапрос вернул больше одной строки
Самая частая ошибка со скалярными подзапросами выглядит так:
SELECT
id,
name,
(
SELECT amount
FROM orders
) AS some_amount
FROM customers;
Внутренний запрос может вернуть много заказов. SQL не понимает, какой именно amount подставить в одну ячейку результата.
В PostgreSQL такая ошибка выглядит так:
ERROR: more than one row returned by a subquery used as an expression
Перевод по-человечески:
Ты используешь подзапрос как одно значение, но он вернул несколько строк.
Исправить можно по-разному — зависит от смысла задачи.
Если нужна сумма:
SELECT
c.id,
c.name,
(
SELECT SUM(o.amount)
FROM orders o
WHERE o.customer_id = c.id
) AS total_spent
FROM customers c;
Если нужен самый большой заказ:
SELECT
c.id,
c.name,
(
SELECT MAX(o.amount)
FROM orders o
WHERE o.customer_id = c.id
) AS max_order_amount
FROM customers c;
Если нужен последний заказ:
SELECT
c.id,
c.name,
(
SELECT o.created_at
FROM orders o
WHERE o.customer_id = c.id
ORDER BY o.created_at DESC
LIMIT 1
) AS last_order_at
FROM customers c;
Когда использовать LIMIT 1
LIMIT 1 нужен, когда подзапрос может вернуть несколько строк, а тебе нужно взять только одну.
Но просто написать LIMIT 1 — не лучшая привычка.
Плохо:
SELECT
c.id,
c.name,
(
SELECT o.created_at
FROM orders o
WHERE o.customer_id = c.id
LIMIT 1
) AS some_order_at
FROM customers c;
Почему плохо? Потому что непонятно, какую именно строку база возьмёт. Сегодня может быть одна, завтра другая. Без сортировки результат не выглядит надёжным.
Лучше так:
SELECT
c.id,
c.name,
(
SELECT o.created_at
FROM orders o
WHERE o.customer_id = c.id
ORDER BY o.created_at DESC
LIMIT 1
) AS last_order_at
FROM customers c;
Теперь смысл понятен: мы берём дату самого свежего заказа.
Правило простое:
если используешь LIMIT 1, почти всегда добавляй ORDER BY.
Так запрос становится детерминированным: база понимает, какую строку считать первой.
Когда агрегаты безопаснее LIMIT 1
Иногда LIMIT 1 вообще не нужен. Если тебе нужно посчитать итог, бери агрегатную функцию.
Агрегаты возвращают одно значение:
SUM — сумму;
COUNT — количество;
AVG — среднее;
MAX — максимум;
MIN — минимум.
Например:
SELECT
c.id,
c.name,
(
SELECT COUNT(*)
FROM orders o
WHERE o.customer_id = c.id
) AS orders_count
FROM customers c;
COUNT всегда вернёт одно значение. Если заказов нет, будет 0.
А вот SUM ведёт себя иначе:
SELECT SUM(amount)
FROM orders
WHERE customer_id = 999;
Если строк нет, SUM вернёт NULL, а не 0.
Поэтому для сумм часто пишут так:
SELECT
c.id,
c.name,
COALESCE((
SELECT SUM(o.amount)
FROM orders o
WHERE o.customer_id = c.id
), 0) AS total_spent
FROM customers c;
Производительность: когда скалярный подзапрос может тормозить
Скалярные подзапросы удобны, но с ними легко написать запрос, который красиво выглядит и грустно работает.
Особенно это касается коррелированных подзапросов.
Например:
SELECT
c.id,
c.name,
(
SELECT SUM(o.amount)
FROM orders o
WHERE o.customer_id = c.id
) AS total_spent
FROM customers c;
Если клиентов тысяча — база может выполнить подзапрос тысячу раз.
Если клиентов миллион — подзапрос может выполниться миллион раз.
PostgreSQL иногда умеет оптимизировать такие конструкции, но рассчитывать на чудо не стоит. На больших таблицах часто лучше переписать запрос через LEFT JOIN и GROUP BY.
SELECT
c.id,
c.name,
COALESCE(SUM(o.amount), 0) AS total_spent
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;
Такой вариант обычно проще оптимизировать на больших данных, особенно если есть индекс по orders.customer_id.
Не нужно бояться скалярных подзапросов. Просто помни правило:
для коротких понятных запросов — отлично, для больших отчётов — проверяй план выполнения через EXPLAIN.
Когда лучше выбрать JOIN
Скалярный подзапрос хорош, когда нужно получить одно дополнительное значение.
Например:
SELECT
c.id,
c.name,
(
SELECT COUNT(*)
FROM orders o
WHERE o.customer_id = c.id
) AS orders_count
FROM customers c;
Но если ты начинаешь добавлять три, четыре, пять подзапросов подряд, это уже тревожный сигнал.
Например:
SELECT
c.id,
c.name,
(
SELECT COUNT(*)
FROM orders o
WHERE o.customer_id = c.id
) AS orders_count,
(
SELECT SUM(o.amount)
FROM orders o
WHERE o.customer_id = c.id
) AS total_spent,
(
SELECT MAX(o.created_at)
FROM orders o
WHERE o.customer_id = c.id
) AS last_order_at
FROM customers c;
Такой запрос уже хочется переписать через JOIN и агрегацию:
SELECT
c.id,
c.name,
COUNT(o.id) AS orders_count,
COALESCE(SUM(o.amount), 0) AS total_spent,
MAX(o.created_at) AS last_order_at
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;
И читается цельнее, и база обычно работает увереннее.
Частые ошибки новичков
Подзапрос возвращает несколько строк
Ты используешь подзапрос как одно значение, а он возвращает несколько строк.
Проблемный пример:
SELECT
c.id,
c.name,
(
SELECT o.amount
FROM orders o
WHERE o.customer_id = c.id
) AS amount
FROM customers c;
Если у клиента несколько заказов, будет ошибка.
Что делать:
- использовать агрегат:
SUM, MAX, MIN, COUNT, AVG;
- добавить
ORDER BY и LIMIT 1;
- заменить скалярный подзапрос на
JOIN, IN или EXISTS.
Подзапрос возвращает несколько колонок
Так нельзя:
SELECT
(
SELECT id, amount
FROM orders
LIMIT 1
) AS order_info;
Скалярный подзапрос должен вернуть одну колонку.
Если нужны и id, и amount, чаще всего нужен JOIN или отдельная логика.
Забыли про NULL
COUNT по пустому набору возвращает 0.
А вот SUM, AVG, MIN, MAX по пустому набору могут вернуть NULL.
Например, если у клиента нет заказов, сумма будет NULL.
Поэтому для числовых отчётов часто используют COALESCE:
SELECT
c.id,
c.name,
COALESCE((
SELECT SUM(o.amount)
FROM orders o
WHERE o.customer_id = c.id
), 0) AS total_spent
FROM customers c;
Используют скалярный подзапрос там, где нужен JOIN
Если нужно достать из связанной таблицы много колонок, скалярный подзапрос — не лучший инструмент.
Плохо масштабируется такая идея:
SELECT
c.id,
c.name,
(
SELECT o.amount
FROM orders o
WHERE o.customer_id = c.id
ORDER BY o.created_at DESC
LIMIT 1
) AS last_order_amount,
(
SELECT o.created_at
FROM orders o
WHERE o.customer_id = c.id
ORDER BY o.created_at DESC
LIMIT 1
) AS last_order_at
FROM customers c;
Если ты несколько раз ходишь в одну и ту же таблицу за разными полями, стоит подумать о JOIN, CTE или оконной функции.
Не понимают, почему запрос медленный
Если внутри подзапроса есть ссылка на наружную таблицу, например c.id, это коррелированный подзапрос.
WHERE o.customer_id = c.id
Он зависит от текущей строки наружного запроса. На маленьких данных это удобно, на больших может быть тяжело.
Если запрос стал медленным, проверь:
- есть ли индекс по колонке связи;
- можно ли переписать через
JOIN;
- что показывает
EXPLAIN.
Короткая шпаргалка
| Задача |
Подходит ли скалярный подзапрос |
| Посчитать одну сумму для каждой строки |
Да |
| Сравнить значение со средним по таблице |
Да |
| Получить дату последнего заказа |
Да, с ORDER BY и LIMIT 1 |
| Проверить, есть ли связанные строки |
Лучше EXISTS |
| Отфильтровать по списку значений |
Лучше IN |
| Забрать много колонок из другой таблицы |
Лучше JOIN |
| Сделать большой отчёт на миллионах строк |
Часто лучше JOIN и GROUP BY |
Мини-резюме
Скалярный подзапрос — это SELECT, который возвращает ровно одно значение: одну строку и одну колонку.
Его удобно использовать там, где SQL ждёт обычное значение:
- в
SELECT, чтобы добавить вычисленную колонку;
- в
WHERE, чтобы сравнить с результатом другого запроса;
- в
UPDATE, чтобы записать вычисленное значение в таблицу.
Главная опасность — случайно вернуть несколько строк. Тогда база выдаст ошибку, потому что не сможет выбрать одно значение из нескольких.
Если значений может быть много, используй агрегат, ORDER BY с LIMIT 1, IN, EXISTS или JOIN — в зависимости от задачи.
И запомни простую мысль: скалярный подзапрос — это маленький запрос, который приносит в большую SQL-картину одну аккуратную деталь.
Скалярный подзапрос звучит как что-то из высшей математики, но в SQL всё гораздо спокойнее.
Скалярный подзапрос — это обычный
SELECT, который возвращает ровно одно значение.То есть результат должен быть таким:
NULL— что угодно.Например:
SELECT AVG(amount) FROM orders;Если этот запрос возвращает одно значение, его можно вставить внутрь другого запроса туда, где обычно стоит обычное значение: число, дата, строка или колонка.
Представь, что SQL-запрос — это анкета. В одну ячейку анкеты можно вписать число вручную, а можно сказать: «Сначала посчитай вот это, а потом подставь результат сюда». Вот это и делает скалярный подзапрос.
Где можно использовать скалярный подзапрос
Скалярный подзапрос можно поставить почти в любое место, где SQL ждёт одно значение.
Чаще всего его используют:
SELECT, чтобы добавить к строке вычисленное значение;WHERE, чтобы сравнить строку с результатом другого запроса;UPDATE, чтобы обновить колонку значением из другого запроса.Например:
SELECT id, name, (SELECT AVG(amount) FROM orders) AS avg_order_amount FROM customers;Внутренний запрос считает среднюю сумму заказа. Наружный запрос показывает клиентов и рядом с каждым клиентом выводит это среднее значение.
Важно: подзапрос внутри круглых скобок должен вернуть одно значение. Не таблицу, не список, не две колонки — именно одно значение.
Зачем нужны скалярные подзапросы
Скалярные подзапросы удобны, когда нужно аккуратно «дописать» к результату одну дополнительную цифру или сравнить данные с вычисленным значением.
Например:
Да, часто то же самое можно сделать через
JOIN,GROUP BYилиCTE. Но для начинающего скалярный подзапрос иногда читается проще: «вот основная строка, а вот маленький запрос, который для неё что-то считает».Базовый синтаксис
Общая форма выглядит так:
SELECT column1, column2, ( SELECT some_column FROM other_table WHERE some_condition ) AS computed_column FROM main_table;В круглых скобках находится подзапрос. SQL выполняет его, получает одно значение и подставляет это значение в результат.
Главное правило:
один скалярный подзапрос = одна колонка и максимум одна строка.
Если подзапрос вернёт несколько строк, база данных не сможет понять, какое именно значение подставлять.
Пример: добавить сумму заказов к каждому клиенту
Допустим, есть таблица клиентов.
И есть таблица заказов.
Хочется получить список клиентов и рядом с каждым показать, сколько денег он потратил.
Запрос:
SELECT c.id, c.name, ( SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = c.id ) AS total_spent FROM customers c;Результат:
Что здесь происходит?
Наружный запрос идёт по таблице
customers. Для каждой строки он видит клиента: Аню, Боба, Веру.А внутри для каждого клиента запускается маленький запрос:
SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = c.id;Для Ани
c.idравен1, значит подзапрос ищет заказы сcustomer_id = 1и получает350.Для Боба
c.idравен2, получается80.Для Веры
c.idравен3, заказов нет. ПоэтомуSUMвозвращаетNULL.Если вместо
NULLхочется видеть0, используйCOALESCE:SELECT c.id, c.name, COALESCE(( SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = c.id ), 0) AS total_spent FROM customers c;Теперь у клиента без заказов будет не
NULL, а0.Коррелированный и некоррелированный подзапрос
Здесь важно понять одну вещь.
Подзапрос может быть независимым или связанным с наружным запросом.
Вот независимый подзапрос:
SELECT * FROM orders WHERE amount > ( SELECT AVG(amount) FROM orders );Внутренний запрос не зависит от текущей строки наружного запроса. Он просто считает среднюю сумму заказа по всей таблице.
Такой подзапрос можно выполнить один раз, получить значение и потом сравнивать с ним каждую строку.
А вот связанный подзапрос:
SELECT c.id, c.name, ( SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = c.id ) AS total_spent FROM customers c;Внутри есть ссылка на
c.idиз наружного запроса. Значит, для каждого клиента значение будет своё.Такой подзапрос называют коррелированным. Он как будто привязан ниточкой к каждой строке внешней таблицы.
Скалярный подзапрос в WHERE
Один из самых понятных сценариев — сравнить строки с каким-то вычисленным значением.
Например, найти заказы дороже среднего:
SELECT id, customer_id, amount FROM orders WHERE amount > ( SELECT AVG(amount) FROM orders );Внутренний запрос считает средний чек. Наружный запрос оставляет только те заказы, где
amountбольше этого среднего значения.Другой пример: найти пользователей, которые зарегистрировались после даты запуска новой версии продукта.
SELECT id, name, created_at FROM users WHERE created_at > ( SELECT created_at FROM milestones WHERE name = 'launch_v2' );Здесь подзапрос должен вернуть одну дату. Наружный запрос сравнивает с ней дату регистрации каждого пользователя.
Но будь внимателен: если в таблице
milestonesокажется две строки сname = 'launch_v2', запрос упадёт с ошибкой. SQL ожидал одну дату, а получил несколько.Скалярный подзапрос в UPDATE
Скалярный подзапрос можно использовать и при обновлении данных.
Например, в таблице
customersесть колонкаtotal_spent, куда нужно записать сумму заказов каждого клиента.UPDATE customers c SET total_spent = COALESCE(( SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = c.id ), 0);Для каждой строки из
customersбаза выполнит подзапрос, найдёт заказы этого клиента, посчитает сумму и запишет результат вtotal_spent.Если заказов нет,
SUMвернётNULL, аCOALESCEзаменит его на0.Чем скалярный подзапрос отличается от IN и EXISTS
Подзапросы бывают разными. Главное отличие — что именно они возвращают.
SELECT,WHERE,SETINWHERE column IN (...)EXISTSWHERE EXISTS (...)Скалярный подзапрос нужен, когда тебе нужно одно значение.
Например:
SELECT * FROM orders WHERE amount > ( SELECT AVG(amount) FROM orders );А
INнужен, когда значений может быть много:SELECT * FROM orders WHERE customer_id IN ( SELECT id FROM customers WHERE city = 'Moscow' );А
EXISTSнужен, когда важен сам факт существования строк:SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );То есть вопрос не в том, «какой подзапрос лучше», а в том, какую задачу ты решаешь.
Если нужно одно значение — скалярный подзапрос.
Если нужен список —
IN.Если нужно проверить наличие —
EXISTS.Ошибка: подзапрос вернул больше одной строки
Самая частая ошибка со скалярными подзапросами выглядит так:
SELECT id, name, ( SELECT amount FROM orders ) AS some_amount FROM customers;Внутренний запрос может вернуть много заказов. SQL не понимает, какой именно
amountподставить в одну ячейку результата.В PostgreSQL такая ошибка выглядит так:
Перевод по-человечески:
Исправить можно по-разному — зависит от смысла задачи.
Если нужна сумма:
SELECT c.id, c.name, ( SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = c.id ) AS total_spent FROM customers c;Если нужен самый большой заказ:
SELECT c.id, c.name, ( SELECT MAX(o.amount) FROM orders o WHERE o.customer_id = c.id ) AS max_order_amount FROM customers c;Если нужен последний заказ:
SELECT c.id, c.name, ( SELECT o.created_at FROM orders o WHERE o.customer_id = c.id ORDER BY o.created_at DESC LIMIT 1 ) AS last_order_at FROM customers c;Когда использовать LIMIT 1
LIMIT 1нужен, когда подзапрос может вернуть несколько строк, а тебе нужно взять только одну.Но просто написать
LIMIT 1— не лучшая привычка.Плохо:
SELECT c.id, c.name, ( SELECT o.created_at FROM orders o WHERE o.customer_id = c.id LIMIT 1 ) AS some_order_at FROM customers c;Почему плохо? Потому что непонятно, какую именно строку база возьмёт. Сегодня может быть одна, завтра другая. Без сортировки результат не выглядит надёжным.
Лучше так:
SELECT c.id, c.name, ( SELECT o.created_at FROM orders o WHERE o.customer_id = c.id ORDER BY o.created_at DESC LIMIT 1 ) AS last_order_at FROM customers c;Теперь смысл понятен: мы берём дату самого свежего заказа.
Правило простое:
если используешь
LIMIT 1, почти всегда добавляйORDER BY.Так запрос становится детерминированным: база понимает, какую строку считать первой.
Когда агрегаты безопаснее LIMIT 1
Иногда
LIMIT 1вообще не нужен. Если тебе нужно посчитать итог, бери агрегатную функцию.Агрегаты возвращают одно значение:
SUM— сумму;COUNT— количество;AVG— среднее;MAX— максимум;MIN— минимум.Например:
SELECT c.id, c.name, ( SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id ) AS orders_count FROM customers c;COUNTвсегда вернёт одно значение. Если заказов нет, будет0.А вот
SUMведёт себя иначе:SELECT SUM(amount) FROM orders WHERE customer_id = 999;Если строк нет,
SUMвернётNULL, а не0.Поэтому для сумм часто пишут так:
SELECT c.id, c.name, COALESCE(( SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = c.id ), 0) AS total_spent FROM customers c;Производительность: когда скалярный подзапрос может тормозить
Скалярные подзапросы удобны, но с ними легко написать запрос, который красиво выглядит и грустно работает.
Особенно это касается коррелированных подзапросов.
Например:
SELECT c.id, c.name, ( SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = c.id ) AS total_spent FROM customers c;Если клиентов тысяча — база может выполнить подзапрос тысячу раз.
Если клиентов миллион — подзапрос может выполниться миллион раз.
PostgreSQL иногда умеет оптимизировать такие конструкции, но рассчитывать на чудо не стоит. На больших таблицах часто лучше переписать запрос через
LEFT JOINиGROUP BY.SELECT c.id, c.name, COALESCE(SUM(o.amount), 0) AS total_spent FROM customers c LEFT JOIN orders o ON o.customer_id = c.id GROUP BY c.id, c.name;Такой вариант обычно проще оптимизировать на больших данных, особенно если есть индекс по
orders.customer_id.Не нужно бояться скалярных подзапросов. Просто помни правило:
для коротких понятных запросов — отлично, для больших отчётов — проверяй план выполнения через
EXPLAIN.Когда лучше выбрать JOIN
Скалярный подзапрос хорош, когда нужно получить одно дополнительное значение.
Например:
SELECT c.id, c.name, ( SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id ) AS orders_count FROM customers c;Но если ты начинаешь добавлять три, четыре, пять подзапросов подряд, это уже тревожный сигнал.
Например:
SELECT c.id, c.name, ( SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id ) AS orders_count, ( SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = c.id ) AS total_spent, ( SELECT MAX(o.created_at) FROM orders o WHERE o.customer_id = c.id ) AS last_order_at FROM customers c;Такой запрос уже хочется переписать через
JOINи агрегацию:SELECT c.id, c.name, COUNT(o.id) AS orders_count, COALESCE(SUM(o.amount), 0) AS total_spent, MAX(o.created_at) AS last_order_at FROM customers c LEFT JOIN orders o ON o.customer_id = c.id GROUP BY c.id, c.name;И читается цельнее, и база обычно работает увереннее.
Частые ошибки новичков
Подзапрос возвращает несколько строк
Ты используешь подзапрос как одно значение, а он возвращает несколько строк.
Проблемный пример:
SELECT c.id, c.name, ( SELECT o.amount FROM orders o WHERE o.customer_id = c.id ) AS amount FROM customers c;Если у клиента несколько заказов, будет ошибка.
Что делать:
SUM,MAX,MIN,COUNT,AVG;ORDER BYиLIMIT 1;JOIN,INилиEXISTS.Подзапрос возвращает несколько колонок
Так нельзя:
SELECT ( SELECT id, amount FROM orders LIMIT 1 ) AS order_info;Скалярный подзапрос должен вернуть одну колонку.
Если нужны и
id, иamount, чаще всего нуженJOINили отдельная логика.Забыли про NULL
COUNTпо пустому набору возвращает0.А вот
SUM,AVG,MIN,MAXпо пустому набору могут вернутьNULL.Например, если у клиента нет заказов, сумма будет
NULL.Поэтому для числовых отчётов часто используют
COALESCE:SELECT c.id, c.name, COALESCE(( SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = c.id ), 0) AS total_spent FROM customers c;Используют скалярный подзапрос там, где нужен JOIN
Если нужно достать из связанной таблицы много колонок, скалярный подзапрос — не лучший инструмент.
Плохо масштабируется такая идея:
SELECT c.id, c.name, ( SELECT o.amount FROM orders o WHERE o.customer_id = c.id ORDER BY o.created_at DESC LIMIT 1 ) AS last_order_amount, ( SELECT o.created_at FROM orders o WHERE o.customer_id = c.id ORDER BY o.created_at DESC LIMIT 1 ) AS last_order_at FROM customers c;Если ты несколько раз ходишь в одну и ту же таблицу за разными полями, стоит подумать о
JOIN,CTEили оконной функции.Не понимают, почему запрос медленный
Если внутри подзапроса есть ссылка на наружную таблицу, например
c.id, это коррелированный подзапрос.WHERE o.customer_id = c.idОн зависит от текущей строки наружного запроса. На маленьких данных это удобно, на больших может быть тяжело.
Если запрос стал медленным, проверь:
JOIN;EXPLAIN.Короткая шпаргалка
ORDER BYиLIMIT 1EXISTSINJOINJOINиGROUP BYМини-резюме
Скалярный подзапрос — это
SELECT, который возвращает ровно одно значение: одну строку и одну колонку.Его удобно использовать там, где SQL ждёт обычное значение:
SELECT, чтобы добавить вычисленную колонку;WHERE, чтобы сравнить с результатом другого запроса;UPDATE, чтобы записать вычисленное значение в таблицу.Главная опасность — случайно вернуть несколько строк. Тогда база выдаст ошибку, потому что не сможет выбрать одно значение из нескольких.
Если значений может быть много, используй агрегат,
ORDER BYсLIMIT 1,IN,EXISTSилиJOIN— в зависимости от задачи.И запомни простую мысль: скалярный подзапрос — это маленький запрос, который приносит в большую SQL-картину одну аккуратную деталь.