Подзапросы в SQL: что нужно знать до начала
За 20 минут вы научитесь читать и писать подзапросы на примере PostgreSQL 16: получите значение для сравнения, отфильтруете строки через EXISTS и соберёте промежуточный результат в FROM. Заодно проверите, где легко получить ошибку или неожиданный ответ.

За 20 минут вы напишете три подзапроса в PostgreSQL 16: получите среднюю цену для сравнения, найдёте товары с продажами и посчитаете продажи по категориям. Для работы понадобится доступ к базе PostgreSQL 16 и SQL-клиент — например, psql или редактор запросов в вашей среде. Скрипт ниже создаёт временные таблицы и не меняет постоянные данные.
Подготовьте данные и запустите примеры
Выполните весь скрипт в одном подключении: временные таблицы существуют только в текущей сессии. В нём есть короткие запросы для трёх основных случаев: подзапрос как одно значение, проверка существования строк и таблица внутри FROM.
CREATE TEMP TABLE products (
product_id integer PRIMARY KEY,
category text NOT NULL,
price numeric(10, 2) NOT NULL
);
CREATE TEMP TABLE sales (
sale_id integer PRIMARY KEY,
product_id integer NOT NULL REFERENCES products(product_id),
quantity integer NOT NULL
);
INSERT INTO products (product_id, category, price) VALUES
(1, 'Книги', 800.00),
(2, 'Книги', 1200.00),
(3, 'Игры', 2500.00),
(4, 'Игры', 4000.00);
INSERT INTO sales (sale_id, product_id, quantity) VALUES
(1, 1, 3),
(2, 1, 2),
(3, 3, 1);
-- Цена выше средней по всем товарам
SELECT product_id, category, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);
-- Товары, для которых есть хотя бы одна продажа
SELECT p.product_id, p.category
FROM products AS p
WHERE EXISTS (
SELECT 1
FROM sales AS s
WHERE s.product_id = p.product_id
);
-- Суммарное количество продаж по категориям
SELECT category, total_quantity
FROM (
SELECT p.category, SUM(s.quantity) AS total_quantity
FROM products AS p
JOIN sales AS s ON s.product_id = p.product_id
GROUP BY p.category
) AS category_sales
ORDER BY category;
Для первых двух запросов должны вернуться товары с ID 3 и 4, затем товары с ID 1 и 3. Последний запрос покажет две категории и суммы: «Игры» — 1, «Книги» — 5. Если результат отличается, проверьте, что скрипт выполнялся целиком и в том же подключении.
Три шага к своему подзапросу
Определите, что должен вернуть внутренний запрос. Один показатель — например, среднюю цену — можно сравнить с колонкой во внешнем запросе. В примере
AVG(price)возвращает одно значение. Проверка: запустите выражение в скобках отдельно; результат должен быть одной строкой и одним столбцом.Выберите условие по смыслу. Если нужно сопоставить значение с результатом запроса, подойдёт сравнение вроде
>. Если важно узнать, нашлась ли связанная запись, используйтеEXISTS. Проверка: во втором запросе товары без продаж не появляются, а товары с одной или несколькими продажами появляются по одному разу.Проверьте форму результата и имя таблицы. Подзапрос в
FROMработает как промежуточная таблица. В PostgreSQL ему нужен псевдоним — здесь этоcategory_sales. Проверка: последний запрос возвращает по одной строке на категорию, а не отдельную строку на каждую продажу.
Подзапрос — не отдельный вид магии. Это обычный запрос, помещённый туда, где внешний запрос ждёт значение, набор значений или таблицу.
Как понять, какой вариант нужен
Одно значение: скалярный подзапрос
Первый запрос сравнивает цену товара со средней ценой всех товаров. Внутренний запрос агрегирует несколько строк в одно значение. Это удобно для порога, который надо вычислить из данных, а не вписывать вручную.
Такой подзапрос должен вернуть не больше одной строки и одного столбца. Если вместо агрегата запрос вернёт несколько строк, PostgreSQL сообщит об ошибке: подзапрос использован как выражение, но вернул несколько строк. Тогда проверьте условие или выберите конструкцию для набора значений.
Наличие связанных строк: EXISTS
EXISTS отвечает на вопрос «нашлась ли хотя бы одна строка». В примере внутреннее условие связывает продажу с текущим товаром через s.product_id = p.product_id. Это коррелированный подзапрос*: он использует значение из внешней строки.
Не заменяйте EXISTS соединением автоматически. Если у товара пять продаж, обычный JOIN вернёт пять строк товара. Для списка товаров без повторов придётся отдельно решать задачу дубликатов, например группировкой. Проверка: временно добавьте вторую продажу для товара с ID 3 — запрос с EXISTS всё равно покажет этот товар один раз.
Результат как таблица: подзапрос в FROM
Третий запрос сначала соединяет товары с продажами и группирует их по категории, затем внешний запрос читает получившийся набор. Такой приём удобен, когда промежуточные данные нужно дальше сортировать, фильтровать или соединять с другими таблицами. У каждой таблицы в FROM должен быть псевдоним; в PostgreSQL без него запрос не выполнится.
Ошибки, которые легко пропустить
Подзапрос вернул несколько строк. Сообщение PostgreSQL обычно указывает, что подзапрос использован как выражение и вернул больше одной строки. Проверьте, ожидалось ли одно значение. Если нужен набор, используйте подходящее сравнение с подзапросом, например
IN, либо измените запрос.В подзапросе появилось NULL, а снаружи стоит NOT IN. Сравнение может дать не «истина», а неизвестный результат, и строки не пройдут фильтр. Для проверки отсутствия связанных записей часто яснее использовать
NOT EXISTSс условием связи. Убедитесь, что обработали возможные NULL и в данных, и в логике запроса.В WHERE не хватает связи с внешней таблицей. Условие
s.product_id = p.product_idсвязывает конкретную продажу с текущим товаром. Без него внутренний запрос может проверять наличие любых продаж вообще, а не продаж именно этого товара. Проверьте связь на строке товара, у которого продаж заведомо нет.В FROM забыли псевдоним. PostgreSQL сообщит об ошибке синтаксиса около конца подзапроса. Добавьте имя после закрывающей скобки, как в
) AS category_sales, и запускайте запрос снова.
Что делать дальше
Возьмите один запрос из своей задачи и сначала запустите внутреннюю часть отдельно. Зафиксируйте, сколько строк и столбцов она возвращает, затем вставьте её во внешний запрос и проверьте результат на данных с нулём, одной и несколькими связанными строками. Когда запрос станет важной частью приложения, сравните планы выполнения через EXPLAIN и проверьте фактическую скорость на данных, похожих на рабочие. Оптимизация подзапросов и планы выполнения — отдельная тема.
*Коррелированный подзапрос — подзапрос, который обращается к колонке строки внешнего запроса. Он проверяется в контексте этих внешних строк, а не сам по себе.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: PostgreSQL 16: подзапросы в выражениях, PostgreSQL 16: таблицы и подзапросы в FROM
Частые вопросы
Нужно ли каждый подзапрос оборачивать в скобки?
Когда использовать EXISTS, а когда IN?
Почему подзапрос вернул ошибку о нескольких строках?
Подзапрос всегда медленнее JOIN?
Подойдут ли примеры для другой СУБД или версии PostgreSQL?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.