0Pricing
Coding Interview Prep · Урок

Переписывание коррелированных подзапросов через JOIN

Преобразуйте коррелированную логику в объединения или оконные функции для повышения производительности.

«Переписывание коррелированных подзапросов через JOIN» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding 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) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Переписывание коррелированных подзапросов через JOIN»?

Преобразуйте коррелированную логику в объединения или оконные функции для повышения производительности. Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Coding Interview Prep?

Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Переписывание коррелированных подзапросов через JOIN»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Coding Interview Prep?

Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Анатомия коррелированного подзапроса
  2. Агрегаты по группам без GROUP BY
  3. Коррелированные EXISTS и NOT EXISTS
  4. Переписывание коррелированных подзапросов через JOIN
← Назад к Coding Interview Prep