Почему перестановка INNER JOIN и LEFT JOIN в цепочке может удалить строки, хотя условия соединения не изменились?
Перестановка INNER JOIN и LEFT JOIN может изменить результат, потому что LEFT JOIN сначала сохраняет строки левой стороны, добавляя NULL для отсутствующих совпадений, а последующий INNER JOIN такие строки удалить. Если сначала выполнить внутреннее соединение внутри правой части, сохранение исходной строки внешней таблицы произойдёт уже после этого удаления.
Внешние соединения появились как расширение обычных реляционных соединений для отчётов, где нужно сохранять сущности без связанных записей: клиентов без заказов, товары без продаж или подразделения без сотрудников. Обычный INNER JOIN возвращает только пары с совпадением и не способен выразить такое требование.
Из-за добавления строк с NULL внешние соединения имеют дополнительные семантические ограничения. Поэтому оптимизатор не может безусловно переставлять их местами с внутренними соединениями, как это часто возможно для нескольких INNER JOIN.
Рассмотрим клиентов, заказы и платежи. Требование может звучать как сохранить всех клиентов, но показать только заказы с найденным платежом.
Если сначала присоединить заказы к клиентам через LEFT JOIN, а затем применить INNER JOIN к платежам, клиент без заказа будет удалён. Если же сначала соединить заказы с платежами, а затем выполнить LEFT JOIN к клиентам, такой клиент сохранится с NULL в столбцах заказа и платежа.
Неверная перестановка особенно опасна при рефакторинге длинного запроса или при ручной оптимизации. Она может незаметно изменить количество клиентов, заказов и итоговые агрегаты.
Упрощённый пример:
В первом запросе клиент с идентификатором 2 сначала получает NULL вместо заказа, после чего INNER JOIN payments не находит совпадение и удаляет строку. Результат содержит только клиента 1.
Во втором запросе сначала формируется набор заказов с платежами. Затем он присоединяется к клиентам через LEFT JOIN, поэтому клиент 2 сохраняется с NULL в полях заказа и платежа.
Ключевой механизм — NULL-расширение внешнего соединения и последующая проверка условия внутреннего соединения. Для строк без заказа значение o.id равно NULL, а обычное сравнение с NULL не даёт истинного результата.
Несколько INNER JOIN часто можно переставлять благодаря ассоциативности реляционного соединения, если не меняются условия, проекции и ограничения. Для комбинаций с LEFT JOIN, RIGHT JOIN или FULL JOIN такая перестановка требует доказательства эквивалентности; простое совпадение текста условий недостаточно.
В отчёте по клиентам разработчик хотел ускорить запрос и перенёс соединение с большой таблицей платежей внутрь производной таблицы заказов. Исходный запрос начинался с клиентов и использовал LEFT JOIN к заказам, поэтому должен был показывать также клиентов без заказов.
Рассматривались два варианта:
клиенты LEFT JOIN заказы INNER JOIN платежи: проще читать, но клиенты без заказов фактически исчезают;LEFT JOIN: сохраняет требуемую полноту списка клиентов, но требует явно проверить границы производной таблицы и условия соединения.Выбран второй вариант, потому что бизнес-требование о сохранении всех клиентов важнее внешнего сходства запросов. После изменения отдельно сравнили количество клиентов, число клиентов без заказов и итоговые суммы; это позволило обнаружить изменение семантики до публикации отчёта.
Нет. Она может изменить результат, если меняется условие соединения, появляются фильтры, зависящие от промежуточной таблицы, используются оконные функции, LIMIT, агрегация или изменяется область видимости выражений. Безопасность перестановки относится только к доказуемо эквивалентным реляционным операциям, а не к любой визуальной перестановке фрагментов SQL.
Условие в ON определяет, какие строки считаются совпавшими. Если совпадения нет, LEFT JOIN всё равно возвращает строку левой стороны и заполняет правые столбцы NULL. Превращение в фактическое внутреннее соединение обычно происходит при фильтрации правых столбцов в WHERE или при последующем INNER JOIN, который не может сопоставить NULL.
Да, но только если преобразование доказано как сохраняющее результат с учётом типов соединений, условий, NULL-семантики и других операторов запроса. Оптимизатор может изменить физический план выполнения, не меняя логическую семантику. Поэтому план может выглядеть иначе, но он не должен превращать сохранение строк внешней таблицы в их удаление.