Перейти к основному содержимому

Подзапросы, EXISTS и IN

Разработчику Аналитику Архитектору

Классификация по позиции

ПозицияНазначениеТребование
SELECTСкалярное значение в проекцииОдна строка, один столбец
FROMПроизводная таблицаОбязателен псевдоним
WHEREФильтрацияIN, EXISTS, сравнение с подзапросом

Коррелированные подзапросы

Подзапрос ссылается на столбцы внешнего запроса. Для каждой строки внешнего запроса подзапрос выполняется заново (если оптимизатор не перепишет его в JOIN).

-- Товары дороже средней цены в своей категории
SELECT name, category, price
FROM products p1
WHERE price > (
SELECT AVG(price)
FROM products p2
WHERE p2.category = p1.category
);

Скаляр в проекции:

SELECT
p.name,
p.price,
(SELECT AVG(price) FROM products p2 WHERE p2.category = p.category) AS avg_in_category,
p.price - (SELECT AVG(price) FROM products p2 WHERE p2.category = p.category) AS deviation
FROM products p
ORDER BY ABS(
p.price - (SELECT AVG(price) FROM products p2 WHERE p2.category = p.category)
) DESC;

EXISTS и NOT EXISTS

Проверяют, вернул ли подзапрос хотя бы одну строку. Содержимое SELECT не важно — обычно пишут SELECT 1.

-- Клиенты, у которых есть хотя бы один заказ
SELECT c.full_name, c.email
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);

-- Клиенты без заказов
SELECT c.full_name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
);

EXISTS устойчив к NULL в подзапросе: важно только наличие строк.


IN и NOT IN

SELECT name, price
FROM products
WHERE category IN ('Книги', 'Канцелярия');

SELECT product_id
FROM order_items
WHERE order_id IN (SELECT order_id FROM orders WHERE status = 'shipped');

Ловушка NOT IN и NULL

Если подзапрос в NOT IN возвращает хотя бы одно NULL в сравниваемом столбце, результат всего предиката — UNKNOWN, и ни одна строка не пройдёт фильтр.

-- Опасно, если customer_id в orders бывает NULL:
SELECT name FROM customers
WHERE customer_id NOT IN (SELECT customer_id FROM orders);

Рекомендация: для проверки отсутствия связи использовать NOT EXISTS или отфильтровать NULL в подзапросе:

WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);

-- либо
WHERE customer_id NOT IN (
SELECT customer_id FROM orders WHERE customer_id IS NOT NULL
);

ALL и ANY

-- Цена выше всех цен в категории «Книги»
SELECT name, price FROM products
WHERE price > ALL (SELECT price FROM products WHERE category = 'Книги');

-- Цена выше хотя бы одной цены поставщика
SELECT name FROM products p
WHERE price > ANY (SELECT supply_price FROM product_suppliers ps WHERE ps.product_id = p.product_id);

> ANY эквивалентно > MIN(подзапрос); > ALL> MAX(подзапрос).


Подзапрос или JOIN?

СценарийКонструкцияПочему
Проверка существования связиEXISTSЯсная семантика, корректно с NULL
Фильтр по фиксированному спискуIN (...)Читаемость
Список с возможным NULLNOT EXISTSКорректность
Нужны столбцы обеих таблицJOINОдно сканирование, проекция
Агрегат по связанной сущностиКоррелированный подзапрос или JOIN + GROUP BYЗависит от плана

Оптимизатор PostgreSQL часто преобразует EXISTS в semi join, коррелированный подзапрос с агрегатом — в JOIN + группировку.


Контрольные вопросы

  1. Чем коррелированный подзапрос отличается от некоррелированного?
  2. Почему NOT IN с NULL в подзапросе даёт пустой результат?
  3. Когда EXISTS предпочтительнее IN?
  4. Может ли оптимизатор не переписать коррелированный подзапрос в JOIN?

См. также


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).