Learning Platform
Урок 08.02 · 20 мин
Средний
Correlated subqueryEXISTSNOT EXISTSAnti-joinSemi-join

Чем корреляция отличается от не-корреляции

В прошлом уроке все наши подзапросы были некоррелированными: они вычислялись один раз, без зависимости от внешней строки. Это позволяло планировщику материализовать результат и подставить его как константу.

Коррелированный подзапрос
— это другая история. Внутри подзапроса есть ссылка на колонку внешней таблицы. Из-за этого логически он выполняется заново для каждой строки внешнего запроса.

Два мира подзапросов

Слева — независимый от внешнего запроса. Справа — связанный через ссылку на внешнюю колонку.

НекоррелированныйSELECT внутри без ссылок наружу
примерWHERE id IN (SELECT customer_id FROM orders WHERE status = 'paid')
выполнениеодин раз, результат материализуется
планHash или InitPlan
КоррелированныйSELECT внутри ссылается на c.id
примерWHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id)
выполнениелогически — для каждой строки c
планSemi-Join / Anti-Join

Запомни характерный признак корреляции — алиас внешней таблицы появляется внутри подзапроса. Если бы во внутреннем SELECT не было ссылки на c.id, подзапрос был бы независимым и вычислялся один раз.

EXISTS — проверка «есть ли хотя бы одна строка»

EXISTS (subquery) возвращает TRUE, если подзапрос вернул хотя бы одну строку, и FALSE — если ни одной. Что именно лежит в этих строках — неважно. Поэтому в подзапросе под EXISTS обычно пишут SELECT 1 — все остальные значения проигнорируются.

Клиенты, у которых был хотя бы один заказ

PostgreSQL

Семантика читается как «оставь клиента c, если для него существует хотя бы один заказ». Это в чистом виде semi-join из реляционной алгебры — операция, которая фильтрует левую таблицу по факту наличия соответствия в правой, не размножая строки.

EXISTS vs JOIN + DISTINCT

Эту же задачу можно решить через JOIN:

Тот же результат — но через JOIN. Зачем здесь DISTINCT?

PostgreSQL

JOIN размножает строки клиента по числу его заказов. Чтобы получить «уникальных клиентов с заказами», приходится накладывать DISTINCT. Это две дополнительные проблемы:

  1. DISTINCT стоит дорого на больших наборах — это сортировка или hash, который СУБД должна построить отдельно.
  2. DISTINCT теряет смысл запроса. Из-за DISTINCT другому разработчику трудно понять, ты убираешь дубликаты данных или дубликаты, порождённые JOIN’ом.

EXISTS решает обе проблемы:

  1. Останавливается на первой найденной строке внутреннего запроса (короткое замыкание). Не нужно сканировать всё.
  2. Семантика «существует» прозрачна — никаких лишних DISTINCT.

Планировщик PostgreSQL обычно переписывает оба варианта в semi-join, и их планы могут совпасть. Но писать EXISTS — это явно сказать планировщику «мне нужен semi-join». Это всегда выигрышнее по читаемости и часто — по плану.

NOT EXISTS — anti-join, безопасный к NULL

NOT EXISTS (subquery) возвращает TRUE, если подзапрос вернул ноль строк. Это anti-join — оставить строки левой таблицы, у которых нет соответствия в правой.

Клиенты без заказов — anti-join через NOT EXISTS

PostgreSQL

Главное преимущество перед NOT IN: NOT EXISTS корректно работает с NULL. Внутри подзапроса используется equi-join по o.customer_id = c.id, который отсекает NULL стандартным образом (NULL = anything это NULL, не TRUE). Никаких сюрпризов вроде «весь результат пустой».

Это — главная причина, по которой в production-коде NOT EXISTS обычно заменяет NOT IN. Сравни поведение на NULL-чувствительных колонках:

Сравни два способа найти клиентов с уникальным birth_year, какого нет у IL-клиентов

PostgreSQL

NOT EXISTS смотрит на сам факт совпадения и не страдает от трёхзначной логики. Ты можешь даже не вычищать NULL из подзапроса — equi-join сделает это сам.

Коррелированный подзапрос в SELECT-списке

Корреляция работает не только под EXISTS. Её можно использовать прямо в SELECT, чтобы посчитать что-то «для текущей строки».

Для каждого клиента — количество его заказов одной коррелированной формулой

PostgreSQL

Это альтернатива LEFT JOIN + GROUP BY + COUNT. Читаемость — на твой вкус, оба варианта планировщик умеет переписывать в один и тот же план. Коррелированная форма часто короче, когда нужен один маленький агрегат «для каждой строки».

Почему «коррелированный = медленный» — это миф

Старая байка: коррелированный подзапрос обязательно медленный, потому что он выполняется N раз. На самом деле PostgreSQL почти никогда не выполняет его N раз буквально. Он переписывает запрос в semi-join (для EXISTS), anti-join (для NOT EXISTS), или lateral nested loop (для коррелированного SELECT).

Проверь сам через EXPLAIN:

Посмотри на план — слово 'Semi Join' должно появиться вместо 'Nested Loop'

PostgreSQL

В выводе ты увидишь Hash Semi Join или Nested Loop Semi Join (зависит от размеров). СУБД сама поняла семантику и развернула её в правильную алгебраическую операцию.

Когда форма всё-таки бьёт по производительности — это случаи, когда планировщик не может переписать. Например, коррелированный подзапрос с агрегатом в SELECT-списке, который ссылается на внешнюю колонку в произвольной арифметике. Тогда — действительно nested loop по строкам. Поправить можно через LATERAL или переписать руками в GROUP BY.

Проверка знанийKnowledge check
Запрос 'SELECT c.* FROM customers c WHERE c.id NOT IN (SELECT customer_id FROM orders)' вдруг начал возвращать ноль строк, хотя раньше работал. Что произошло?
ОтветAnswer
В orders.customer_id появился NULL. NOT IN с NULL в подзапросе ломается из-за трёхзначной логики: x NOT IN (..., NULL) сводится к x != NULL, что NULL, и весь предикат становится NULL — строка отсекается. Лечение: переписать через NOT EXISTS, который смотрит на equi-join и корректно отбрасывает NULL без побочного эффекта. Или добавить WHERE customer_id IS NOT NULL внутри подзапроса. Первое — рекомендуемая практика.
Коррелированный подзапрос как Nested Loop join

Чек-лист

  • Коррелированный подзапрос ссылается на алиас внешней таблицы. Это его единственный отличительный признак.
  • EXISTS (...) — это semi-join: «оставь строку, если в правой таблице есть хотя бы одно совпадение». Подзапрос обычно пишут как SELECT 1.
  • EXISTS обычно лучше JOIN + DISTINCT — нет накладных расходов на дедупликацию, прозрачнее семантика.
  • NOT EXISTS — anti-join, безопасный к NULL. В production-коде он почти всегда лучше NOT IN.
  • Планировщик переписывает EXISTS/NOT EXISTS в Semi/Anti Join. Коррелированный подзапрос — это не автоматически медленно.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какой главный синтаксический признак коррелированного подзапроса?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6