Подзапросы, 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.
Код ITЗагрузка примера кода…
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 (...) | Читаемость |
Список с возможным NULL | NOT EXISTS | Корректность |
| Нужны столбцы обеих таблиц | JOIN | Одно сканирование, проекция |
| Агрегат по связанной сущности | Коррелированный подзапрос или JOIN + GROUP BY | Зависит от плана |
Оптимизатор PostgreSQL часто преобразует EXISTS в semi join, коррелированный подзапрос с агрегатом — в JOIN + группировку.
Контрольные вопросы
- Чем коррелированный подзапрос отличается от некоррелированного?
- Почему
NOT INсNULLв подзапросе даёт пустой результат? - Когда
EXISTSпредпочтительнееIN? - Может ли оптимизатор не переписать коррелированный подзапрос в
JOIN?
См. также
- Чтение и анализ сложных SQL-запросов
- Алиасы, JOIN и объединение таблиц
- Практикум shop_data
- SQL — реальные кейсы —
NOT EXISTS, клиенты без заказов, подзапрос для среднего чека