Переписывание коррелированных подзапросов через JOIN
Преобразуйте коррелированную логику в объединения или оконные функции для повышения производительности.
«Переписывание коррелированных подзапросов через JOIN» — бесплатный урок SQL Interview Prep на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения SQL Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс SQL Interview Prep содержит 4 уроков всего.
Зачем вообще переписывать
Коррелированные подзапросы легко читать, но они могут работать медленно: внутренний запрос может выполняться один раз для каждой внешней строки. На собеседованиях часто просят переписать такой запрос с помощью соединения или оконной функции, чтобы повысить производительность.
Цель — получить тот же результат за один проход по данным вместо повторного сканирования внутреннего запроса.
Знание двух или трёх шаблонов переписывания и понимание того, когда каждый из них сохраняет корректность, — важный навык специалиста среднего уровня.
Шаблон 1: EXISTS в INNER JOIN
Коррелированный EXISTS, проверяющий наличие хотя бы одного совпадения, часто можно заменить на INNER JOIN.
Но будьте осторожны: соединение может создать дубликаты внешних строк, если совпадает несколько внутренних строк. Добавьте DISTINCT или используйте агрегацию, чтобы снова получить по одной строке для каждого внешнего ключа.
-- Correlated EXISTS
SELECT c.customer_id, c.name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o
WHERE o.customer_id = c.customer_id);
-- Join rewrite (DISTINCT avoids dupes from fan-out)
SELECT DISTINCT c.customer_id, c.name
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id;Проблема размножения строк
Самая распространённая ошибка при переписывании — забыть о размножении строк. EXISTS возвращает каждого клиента один раз, независимо от количества его заказов. Наивное соединение возвращает по одной строке для каждого заказа, завышая результаты подсчётов.
Если следующий этап выполняет COUNT(*) или SUM(amount) над таким результатом соединения без тщательной группировки, числа будут неверными.
Всегда спрашивайте себя: может ли соединение размножить строки? Если да, используйте DISTINCT или GROUP BY, чтобы снова объединить их.
Шаблон 2: NOT EXISTS в LEFT JOIN / IS NULL
Переписывание в антисоединение — гарантированный шаблон на собеседованиях. Коррелированный NOT EXISTS превращается в LEFT JOIN, при котором правая сторона имеет значение NULL.
Для внешних строк без совпадений справа появляются значения NULL; фильтрация по этому NULL оставляет ровно строки без совпадений.
-- Correlated NOT EXISTS
SELECT c.customer_id FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o
WHERE o.customer_id = c.customer_id);
-- LEFT JOIN / IS NULL rewrite
SELECT c.customer_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.customer_id IS NULL;Выберите столбец, не допускающий NULL, для проверки
При переписывании через LEFT JOIN / IS NULL проверяйте столбец правой стороны, который никогда не бывает NULL при реальном совпадении, в идеале ключ соединения или первичный ключ.
Если проверить столбец, допускающий NULL, невозможно отличить настоящее отсутствие совпадения (строки нет) от совпавшей строки, в которой это поле просто содержит NULL. Такая ошибка возвращает неверные строки.
Использование ключа соединения (здесь o.customer_id) или o.order_id гарантирует, что NULL означает «подходящей строки нет».
Шаблон 3: скалярная агрегация в JOIN + GROUP BY
Коррелированную агрегацию в SELECT можно заменить соединением с сгруппированным подзапросом (производной таблицей).
Вычислите агрегат для каждой группы один раз, а затем присоедините его обратно к строкам с деталями. Внутренний запрос выполнится один раз, а не для каждой строки.
-- Correlated scalar aggregate
SELECT e1.name,
(SELECT MAX(e2.salary) FROM employees e2
WHERE e2.dept_id = e1.dept_id) AS dept_max
FROM employees e1;
-- Join + GROUP BY rewrite
SELECT e.name, m.dept_max
FROM employees e
JOIN (SELECT dept_id, MAX(salary) AS dept_max
FROM employees GROUP BY dept_id) m
ON m.dept_id = e.dept_id;Шаблон 4: переписывание с оконной функцией
Часто самым понятным вариантом переписывания оказывается оконная функция. MAX(salary) OVER (PARTITION BY dept_id) полностью заменяет коррелированную агрегацию, и соединение не требуется.
Она вычисляет значение группы за один проход и сохраняет каждую строку с деталями. Обычно именно такой ответ интервьюеры больше всего хотят увидеть в запросах для аналитики.
SELECT name,
dept_id,
salary,
MAX(salary) OVER (PARTITION BY dept_id) AS dept_max
FROM employees;Переписывание для выбора максимального значения в каждой группе
Коррелированный подзапрос, выбирающий первую строку в каждой группе (salary = MAX per dept), удобно переписывается с помощью ROW_NUMBER.
Разделите строки по группе, упорядочьте их по показателю и оставьте строки с рангом 1. Используйте RANK, если хотите получить все строки с одинаковым максимальным значением.
SELECT name, dept_id, salary
FROM (
SELECT name, dept_id, salary,
ROW_NUMBER() OVER (PARTITION BY dept_id
ORDER BY salary DESC) AS rn
FROM employees
) t
WHERE rn = 1;Когда NOT не следует переписывать
Переписывание не всегда улучшает запрос. Оставьте коррелированный подзапрос, если:
- Внешний набор очень мал, поэтому затраты на обработку каждой строки несущественны.
- Коррелированный столбец хорошо проиндексирован, а оптимизатор уже преобразует запрос в эффективное полусоединение.
- В сопровождаемом коде удобство чтения важнее микрооптимизации.
Современные оптимизаторы часто автоматически преобразуют EXISTS в полусоединение. Скажите, что перед предположением о пользе переписывания Вы проверите план с помощью EXPLAIN.
Проверка эквивалентности
После любого переписывания убедитесь, что оно возвращает те же строки и то же количество строк, что и исходный запрос.
- Проверьте совпадение количества строк.
- Проверьте, что из-за размножения строк при соединении не появились дубликаты.
- Проверьте, что особые случаи с NULL и пустыми группами по-прежнему обрабатываются правильно.
Быстрый способ: выполните обе версии и примените EXCEPT к ним в обоих направлениях; пустой результат означает, что они совпадают. Интервьюеры ценят, когда Вы проверяете результат, а не делаете предположения.
SELECT customer_id FROM query_a
EXCEPT
SELECT customer_id FROM query_b;
-- and the reverse; both empty => equivalentПереписывание IN в JOIN
Некоррелированный подзапрос с IN тоже часто можно переписать через соединение, но предупреждение о размножении строк остаётся в силе. IN удаляет дубликаты при проверке принадлежности, а соединение — нет.
Если во внутреннем списке есть повторяющиеся ключи, соединение повторит внешние строки. Используйте DISTINCT во внутренней части или в итоговом результате, чтобы сохранить семантику IN.
-- IN subquery
SELECT c.name FROM customers c
WHERE c.customer_id IN (SELECT o.customer_id FROM orders o);
-- Join rewrite, de-duplicated to match IN
SELECT DISTINCT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id;Быстрая проверка
Выберите правильный вариант соединения для коррелированного антисоединения NOT EXISTS.
Итоги: переписывание коррелированных подзапросов через соединения
Основные выводы:
EXISTS→INNER JOIN(добавьте DISTINCT, чтобы избежать дубликатов из-за размножения строк).NOT EXISTS→LEFT JOIN ... WHERE key IS NULL(проверяйте столбец, который не допускает NULL).- Коррелированная скалярная агрегация →
JOINсо сгруппированной производной таблицей или, что ещё лучше, оконная функция. - Максимальное значение в каждой группе →
ROW_NUMBER(илиRANKдля одинаковых значений). - Проверьте эквивалентность и изучите план с помощью
EXPLAIN, прежде чем считать переписанный вариант более быстрым.
Знание обеих форм и ловушки с размножением строк — именно это проверяют на собеседованиях специалистов среднего уровня.
Часто задаваемые вопросы
Урок «Переписывание коррелированных подзапросов через JOIN» бесплатный?
Да — полный текст урока «Переписывание коррелированных подзапросов через JOIN» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс SQL Interview Prep, подпишись на CoddyKit PRO. Курс SQL Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Переписывание коррелированных подзапросов через JOIN»?
Преобразуйте коррелированную логику в объединения или оконные функции для повышения производительности. Ты практикуешь SQL Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать SQL Interview Prep?
Предыдущий опыт не требуется. SQL Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Переписывание коррелированных подзапросов через JOIN»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке SQL Interview Prep?
Да. Каждый урок SQL Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Анатомия коррелированного подзапроса
- Агрегаты по группам без GROUP BY
- Коррелированные EXISTS и NOT EXISTS
- Переписывание коррелированных подзапросов через JOIN