Программирование SQLОсновы SQL и реляционная модельРазработчик SQL и реляционных баз данных

Объясните механизм, из за которого перестановка LEFT JOIN и INNER JOIN может изменить результат запроса.

Объясните механизм, из-за которого перестановка LEFT JOIN и INNER JOIN может изменить результат запроса.

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Перестановка LEFT JOIN и INNER JOIN может изменить результат, потому что LEFT JOIN сохраняет строки левой стороны, добавляя для отсутствующих совпадений значения NULL, а INNER JOIN такие строки отбрасывает. Поэтому группировка соединений определяет, на каком этапе строка будет сохранена или удалена.

Исторический контекст

Реляционная алгебра изначально описывала операции над отношениями как над множествами, включая внутреннее соединение. В SQL появились внешние соединения, чтобы сохранять строки, для которых связанной записи нет, например всех клиентов, включая клиентов без заказов.

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

Постановка проблемы

Оптимизатор СУБД может менять порядок операций, чтобы использовать индексы, уменьшить промежуточные результаты или выбрать более эффективный план. Но такое преобразование допустимо только тогда, когда оно сохраняет семантику исходного запроса.

Если применить INNER JOIN до LEFT JOIN, строки могут исчезнуть до того, как внешнее соединение получит возможность их сохранить. Ошибка особенно опасна при отчётах, где отсутствие связанных данных само по себе является значимым результатом.

Подробное решение

LEFT JOIN гарантирует сохранение каждой строки левого отношения. Если совпадение не найдено, СУБД формирует расширенную строку, в которой столбцы правого отношения имеют значение NULL.

INNER JOIN, напротив, оставляет только строки с совпадением. Поэтому следующие логические схемы неэквивалентны:

-- Сначала сохраняем клиентов, затем требуем заказ с оплатой SELECT c.id FROM customers AS c LEFT JOIN orders AS o ON o.customer_id = c.id INNER JOIN payments AS p ON p.order_id = o.id; -- Сначала находим заказы с оплатой, затем сохраняем клиентов без таких заказов SELECT c.id FROM customers AS c LEFT JOIN ( orders AS o INNER JOIN payments AS p ON p.order_id = o.id ) ON o.customer_id = c.id;

В первом варианте INNER JOIN payments отбрасывает строки, где orders не найден: там o.id равен NULL. Фактически внешний эффект LEFT JOIN для клиентов без заказов теряется.

Во втором варианте сначала формируется набор заказов с оплатой, а затем LEFT JOIN сохраняет каждого клиента, даже если подходящего заказа нет. У такого клиента присоединённые столбцы будут NULL.

Следовательно, оптимизатор не может произвольно переставлять внешние и внутренние соединения. Безопасность преобразования зависит от типа соединения, условий ON, последующих фильтров, ограничений целостности и иногда от доказуемой гарантии существования совпадения.

Ситуация из практики

В отчёте требовалось показать всех клиентов и сумму только оплаченных заказов. Разработчик записал цепочку LEFT JOIN orders, затем INNER JOIN payments, ожидая, что клиенты без оплаты останутся в отчёте. На деле внутреннее соединение удалило строки с отсутствующими заказами.

Можно было заменить INNER JOIN на LEFT JOIN, но тогда неоплаченные заказы тоже могли попасть в промежуточный набор и повлиять на агрегирование. Другой вариант — поместить соединение заказов с оплатами во вложенный набор и присоединить его к клиентам через LEFT JOIN; он явно выражает требуемую семантику.

Выбран был второй вариант: сначала формировались только оплаченные заказы, затем они присоединялись к полному списку клиентов. Это сохранило клиентов без оплат и исключило неоплаченные заказы из расчётов; заодно структура запроса стала понятнее для оптимизатора и сопровождающих разработчиков.

Что кандидаты часто упускают

1. Можно ли заменить LEFT JOIN на INNER JOIN, если после него есть условие по правой таблице?

Да, если условие в WHERE требует ненулевого совпадения по правой таблице, внешнее соединение фактически превращается во внутреннее. Строки без совпадения получают NULL, а предикат в WHERE не принимает для них значение TRUE, поэтому они отбрасываются.

Однако это нужно рассматривать как изменение семантики, а не как безусловную оптимизацию. Перенос такого условия в ON обычно сохраняет строки левой таблицы и потому даёт другой результат.

2. Всегда ли внутренние соединения можно переставлять местами?

Нет, хотя внутреннее соединение обладает алгебраическими свойствами, позволяющими многие перестановки. На результат могут влиять дубликаты, условия соединения, выражения с недетерминированными функциями, пользовательские функции и особенности типов данных.

Для обычных эквисоединений над таблицами без побочных эффектов оптимизатор обычно может менять порядок, потому что итоговый набор строк сохраняется, а промежуточные наборы меняются только количественно. Для внешних соединений такого общего правила нет.

3. Может ли ограничение внешнего ключа сделать перестановку безопасной?

Иногда да. Если ограничения гарантируют, что для каждой строки левой таблицы обязательно существует ровно одно подходящее значение в правой таблице, часть внешнего соединения может быть семантически эквивалентна внутреннему.

Но наличие внешнего ключа само по себе не означает, что любая перестановка безопасна: важны направление ссылки, допускаются ли NULL, уникальность целевой стороны и остальные условия запроса. Такое преобразование должен обосновывать оптимизатор с учётом схемы, а не разработчик предполагать по внешнему виду запроса.