PostgreSQL
Подзапросы: EXISTS, IN, ANY, ALL
JOIN - основной инструмент связи таблиц. Но есть класс вопросов, которые естественнее выражаются иначе: «найди сотрудников, чья зарплата выше средней по их отделу» или «отдели заказы без соответствующего клиента». Подзапросы позволяют строить запросы из запросов - как функции из функций. Главное: знать, когда это эффективно, а когда это ловушка.
- **Антифрод:** найти транзакции, сумма которых выше среднего по пользователю за последние 30 дней - коррелированный подзапрос или window function
- **Отчётность:** отделы без ни одного сотрудника с определённым навыком - NOT EXISTS намного безопаснее NOT IN при nullable данных
- **Data quality:** найти записи-сироты (заказы без клиента, позиции без товара) - LEFT JOIN + IS NULL или NOT EXISTS
Скалярные подзапросы
**Скалярный подзапрос** - это SELECT внутри другого выражения, который возвращает ровно одну строку и один столбец. Он может появляться везде, где допускается скалярное значение: в SELECT, WHERE, HAVING, ORDER BY.
**Важно:** если скалярный подзапрос возвращает NULL (нет строк) или более одной строки - PostgreSQL выбросит ошибку. Для безопасности используйте `LIMIT 1` или агрегатную функцию.
Скалярный подзапрос в SELECT выполняется:
Табличные подзапросы в FROM
Подзапрос в предложении FROM возвращает виртуальную таблицу - **derived table** (или inline view). К ней можно обращаться как к обычной таблице: делать JOIN, GROUP BY, WHERE. Это удобный способ разбить сложный запрос на читаемые слои.
**ANY и ALL:** `> ANY(subquery)` истинно, если значение больше хотя бы одного результата подзапроса. `> ALL(subquery)` - если больше всех. `= ANY(subquery)` эквивалентен `IN (subquery)`.
Подзапрос в предложении FROM обязательно должен:
Коррелированные подзапросы
**Коррелированный подзапрос** ссылается на столбцы из внешнего запроса. Он выполняется заново для *каждой строки* внешнего запроса. Это даёт гибкость, но может быть медленным - N строк во внешнем запросе = N выполнений подзапроса.
Коррелированные подзапросы незаменимы для «найти по своей группе», но планировщик PostgreSQL часто умеет их переписать в JOIN или window function. `EXPLAIN ANALYZE` покажет, произошла ли такая оптимизация.
**LATERAL:** расширение коррелированных подзапросов для FROM. `FROM employees e, LATERAL (SELECT ... WHERE dept_id = e.id LIMIT 3) top3` - позволяет коррелированный подзапрос прямо в FROM, возвращая несколько строк на каждую строку внешней таблицы.
Коррелированный подзапрос с 10 000 строк во внешней таблице выполнится сколько раз?
EXISTS vs IN: когда что использовать
Вопрос «есть ли сотрудники в этом отделе» можно задать двумя способами: `IN (subquery)` и `EXISTS (subquery)`. Семантически они эквивалентны для NOT NULL данных, но работают по-разному и ведут себя по-разному при NULL.
**PostgreSQL оптимизация:** планировщик часто трансформирует `IN (subquery)` в semi-join, а `EXISTS` - тоже в semi-join. На практике в PostgreSQL 14+ разница в плане между IN и EXISTS часто нулевая. Главное правило - используйте `NOT EXISTS` вместо `NOT IN` при возможных NULL.
Запрос `WHERE id NOT IN (SELECT manager_id FROM dept)` вернул 0 строк, хотя данные явно должны быть. Наиболее вероятная причина:
Производительность подзапросов
Подзапросы - не волшебство: неправильно написанный подзапрос может превратить запрос из миллисекунд в минуты. Ключевые паттерны: коррелированный подзапрос в SELECT вместо JOIN, NOT IN с большими таблицами, подзапрос без индекса на условии корреляции.
PostgreSQL планировщик умеет трансформировать многие подзапросы в JOIN автоматически. Но гарантии нет - зависит от статистики, размера таблиц, наличия индексов. `EXPLAIN ANALYZE` - единственный способ убедиться в эффективности плана.
**Полезная эвристика:** если подзапрос в WHERE возвращает большой набор данных и не является коррелированным - планировщик скорее всего уже превратил его в HashJoin. Если же подзапрос коррелированный и в SELECT - это почти наверняка N лишних запросов к таблице.
EXISTS всегда быстрее IN
В PostgreSQL 14+ планировщик трансформирует оба в semi-join. Реальная разница: NOT IN опасен с NULL, NOT EXISTS безопасен. По скорости - смотрите EXPLAIN ANALYZE для конкретного случая.
Миф пришёл из Oracle 90-х, где EXISTS и IN обрабатывались по-разному. PostgreSQL уже давно объединяет их в один план. Важна корректность (NULL-безопасность), а не предположения о скорости.
Запрос SELECT name, (SELECT dept_name FROM departments WHERE id = e.dept_id) FROM employees e - в чём его главная проблема?
Подзапросы в PostgreSQL
- Скалярный подзапрос: ровно 1 строка, 1 столбец - допустим в SELECT/WHERE/HAVING/ORDER BY
- Derived table (FROM subquery): виртуальная таблица, обязателен алиас; основа для сложных агрегаций
- Коррелированный подзапрос: ссылается на внешнюю строку, выполняется N раз - гибко, но следите за планом
- NOT IN с NULL в подзапросе возвращает 0 строк - всегда используйте NOT EXISTS для nullable столбцов
- EXPLAIN ANALYZE - единственный способ проверить, что план действительно эффективен
Связанные темы
Подзапросы - строительный блок для CTE и оконных функций.
- CTE и WITH RECURSIVE — CTE - именованные подзапросы, более читаемые и иногда материализованные
- Оконные функции — Многие коррелированные подзапросы заменяются оконными функциями с лучшей производительностью
- EXPLAIN и планировщик — Анализ плана выполнения подзапросов - ключевой инструмент оптимизации
Вопросы для размышления
- Коррелированный подзапрос в WHERE выполняется N раз. При каких условиях это может быть быстрее JOIN?
- NOT IN безопасен только с NOT NULL столбцами. Как проверить через EXPLAIN ANALYZE, что планировщик правильно обработал NULL?
- В каких случаях derived table в FROM предпочтительнее CTE с точки зрения планировщика PostgreSQL?
Связанные уроки
- pg-07-joins — Many subqueries can be rewritten as JOINs and vice versa; knowing both lets you pick for readability and performance
- pg-10-cte — CTEs are named subqueries; mastering inline subqueries is prerequisite to understanding CTE semantics
- pg-08-aggregation — Scalar subqueries in HAVING are a common pattern; aggregation context needed
- pg-06-select — Subqueries are nested SELECTs; basic SELECT syntax must be solid first
- db-05-sql-basics