Подзапросы в SQL: пошаговая инструкция для PostgreSQL
За 15–20 минут вы научитесь использовать подзапросы в PostgreSQL 16: получать одно значение, фильтровать строки через IN и проверять связанные записи через EXISTS. В конце — разбор ошибок, которые легко принять за проблему в базе.

За 15–20 минут вы научитесь писать три вида подзапросов в PostgreSQL 16: получать одно значение, выбирать строки по списку и проверять наличие связанных записей. Все примеры можно выполнить в одной сессии psql; вместо реального магазина используется небольшая временная таблица заказов.
Что понадобится
Нужен доступ к PostgreSQL 16 и SQL-клиенту: подойдёт psql, pgAdmin или другой клиент, который отправляет запросы в базу. Для примеров достаточно прав на создание временных таблиц. Скрипт создаёт таблицы только на время соединения и не меняет постоянные данные.
Подзапрос1 — запрос, вложенный в другой запрос. Он может вернуть одно значение, набор значений или таблицу; от этого зависит, где его можно использовать.
Шаг 1. Подготовьте данные
Скопируйте и выполните запрос целиком. В нём есть две временные таблицы: покупатели и их заказы.
CREATE TEMP TABLE customers (
customer_id integer PRIMARY KEY,
customer_name text NOT NULL
);
CREATE TEMP TABLE orders (
order_id integer PRIMARY KEY,
customer_id integer NOT NULL REFERENCES customers(customer_id),
amount numeric(10, 2) NOT NULL,
status text NOT NULL
);
INSERT INTO customers (customer_id, customer_name) VALUES
(1, 'Анна'),
(2, 'Борис'),
(3, 'Вера');
INSERT INTO orders (order_id, customer_id, amount, status) VALUES
(101, 1, 1200.00, 'paid'),
(102, 1, 800.00, 'cancelled'),
(103, 2, 2500.00, 'paid');
Проверка: выполните SELECT * FROM orders;. Должны появиться три строки. Если клиент сообщает, что временная таблица уже существует, вы повторно запустили скрипт в той же сессии. Откройте новое соединение или удалите временные таблицы командой DROP TABLE orders, customers; и запустите скрипт снова.
Шаг 2. Получите одно значение
Скалярный подзапрос возвращает одно значение. Например, можно найти заказы, сумма которых выше средней суммы всех заказов:
SELECT order_id, customer_id, amount
FROM orders
WHERE amount > (
SELECT AVG(amount)
FROM orders
);
Внутренний запрос вычисляет среднее, внешний сравнивает с ним сумму каждого заказа. Для этих данных среднее равно 1500, поэтому запрос вернёт заказ 103 на 2500. Проверка: в результате должна быть одна строка. Подзапрос с агрегатной функцией без GROUP BY возвращает одну строку даже для пустой таблицы: значение в ней может быть NULL.
Перед тем как ставить подзапрос в условие сравнения, проверьте не только его значение, но и то, сколько строк он возвращает. Для оператора
=это принципиально.
Шаг 3. Отберите строки по списку
Если внутренний запрос возвращает несколько значений, используйте IN. Например, выведите покупателей, у которых есть хотя бы один оплаченный заказ:
SELECT customer_id, customer_name
FROM customers
WHERE customer_id IN (
SELECT customer_id
FROM orders
WHERE status = 'paid'
);
Подзапрос возвращает идентификаторы покупателей с оплаченными заказами — в этом наборе Анна и Борис. Проверка: внешний запрос вернёт две строки. Самому вложенному запросу необязательно быть уникальным: повторяющиеся значения в наборе не дублируют строки внешней таблицы.
Шаг 4. Проверьте связанные записи через EXISTS
EXISTS проверяет, вернул ли подзапрос хотя бы одну строку. Так можно найти покупателей с заказом, который ещё не оплачен:
SELECT c.customer_id, c.customer_name
FROM customers AS c
WHERE EXISTS (
SELECT 1
FROM orders AS o
WHERE o.customer_id = c.customer_id
AND o.status <> 'paid'
);
Подзапрос связан с внешней строкой условием o.customer_id = c.customer_id: для каждого покупателя проверяется его заказ. Проверка: вернётся одна строка — Анна, потому что у неё есть отменённый заказ. Внутри EXISTS обычно пишут SELECT 1: важно наличие строки, а не выбранное значение.
Шаг 5. Разберитесь, если запрос не сработал
- ERROR: more than one row returned by a subquery used as an expression. Подзапрос в сравнении вроде
=вернул несколько строк. Если нужен набор, используйтеINилиEXISTS; если одно значение — измените условие или вычисление так, чтобы результат был однозначным. - ERROR: relation "orders" does not exist. Таблица не найдена в текущей сессии: временные таблицы исчезают после закрытия соединения. Выполните подготовительный скрипт заново в том же окне соединения.
- ERROR: column ... does not exist. Проверьте название столбца и его псевдоним. В примере у таблиц заданы сокращения
cиo; столбец нужно указывать с соответствующим префиксом, напримерo.customer_id. - Запрос выполнился, но вернул ноль строк. Проверьте значения в условии: в примере статус записан как
'paid'. SQL сравнивает его с точным значением в столбце.
Что делать дальше
Подставьте вместо учебных таблиц свои, но сначала проверьте запрос на ограниченной выборке. Если он работает медленно на реальных данных, посмотрите план через EXPLAIN, а не заменяйте подзапрос на соединение наугад: оптимизатор PostgreSQL может преобразовать запрос при планировании. Особенно внимательно проверьте коррелированные подзапросы на больших таблицах и сравните план на данных, близких к рабочему объёму. Индексы и оптимизация производительности — следующий отдельный шаг.
1 Скалярный подзапрос — вложенный запрос, который используется как одно значение. Если он вернёт больше одной строки, PostgreSQL сообщит об ошибке.
FAQ
Чем подзапрос отличается от JOIN?
Подзапрос подходит, когда нужно получить значение, набор для фильтрации или проверить наличие записей. JOIN соединяет строки таблиц и позволяет вывести поля из обеих. Какой вариант лучше, зависит от результата, который нужен, и плана выполнения.
Когда использовать IN, а когда EXISTS?
IN проверяет, входит ли значение в набор результатов. EXISTS проверяет, нашлась ли хотя бы одна строка по условию. Для связанных записей, как в примере с заказами, EXISTS явно показывает проверку наличия.
Почему подзапрос возвращает несколько строк и вызывает ошибку?
Так происходит, когда запрос в позиции одного значения находит больше одной строки, например при сравнении через =. Используйте IN, если сравниваете со списком, или уточните условие, чтобы выбрать одно значение.
Можно ли выполнять примеры в другой версии PostgreSQL?
Синтаксис примеров рассчитан на PostgreSQL 16 и использует базовые возможности SQL. В другой версии проверьте её документацию и выполните запросы на тестовой базе: детали поведения и сообщения об ошибках могут отличаться.
Что делать, если таблица исчезла после перезапуска клиента?
В примере используются временные таблицы, которые существуют только в текущем соединении. Запустите скрипт создания таблиц и вставки данных в новой сессии. Для постоянной работы используйте таблицы своей базы, а не временные учебные.
IT-архитектор / SQL-оптимизатор: Поиск N+1, оптимизация запросов, code review, поиск багов.
Спросить за 20 ₽Источники: PostgreSQL 16: подзапросы, PostgreSQL 16: SELECT
Частые вопросы
Чем подзапрос отличается от JOIN?
Когда использовать IN, а когда EXISTS?
Почему подзапрос возвращает несколько строк и вызывает ошибку?
Можно ли выполнять примеры в другой версии PostgreSQL?
Что делать, если таблица исчезла после перезапуска клиента?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.