SQLsubqueryscalartutorial

Что такое скалярный подзапрос в SQL?

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

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

Скалярный подзапрос звучит как что-то из высшей математики, но в 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-тренажёре с мгновенной проверкой и подсказками.

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