Почему СУБД может выполнить внутренние соединения не в порядке, указанном в тексте SQL-запроса?
Потому что SQL задаёт требуемый результат, а не обязательный порядок вычислений. Для цепочки внутренних соединений СУБД обычно может изменить порядок их выполнения, сохранив логический результат, и выбрать более дешёвый план на основе статистики.
Такое преобразование корректно только при сохранении семантики запроса. Внешние соединения, ограничение числа строк, оконные вычисления и выражения с наблюдаемыми побочными эффектами могут сделать перестановку недопустимой или ограничить её.
Реляционная модель отделяет описание результата от способа его получения. Реляционная алгебра предоставляет операции над отношениями, а не инструкции, в каком физическом порядке читать таблицы или соединять их.
Это разделение позволило СУБД применять оптимизацию: выбирать индексы, алгоритмы соединения и порядок операций независимо от написанного запроса. Исходная проблема — получить тот же результат с меньшими затратами памяти, диска и процессорного времени.
Рассмотрим запрос, который соединяет клиентов, заказы и позиции заказов. Если сначала соединить самые большие таблицы, промежуточный результат может оказаться огромным. Если сначала применить селективный фильтр к клиентам или использовать небольшую таблицу как основу соединения, объём обрабатываемых данных может существенно уменьшиться.
Однако механическая перестановка опасна. Внешнее соединение сохраняет строки без пары, поэтому его порядок влияет на результат. Кроме того, LIMIT, оконные функции и некоторые вычисляемые выражения зависят от конкретной последовательности обработки.
Для обычных INNER JOIN порядок соединения логически можно менять, если сохраняются те же таблицы и условия соединения. В терминах реляционной алгебры внутреннее соединение обладает свойствами коммутативности и ассоциативности: результат соединения A с B эквивалентен соединению B с A, а группировку нескольких внутренних соединений можно менять.
В SQL учитывается не только множество строк, но и их кратность. Это не отменяет перестановку внутренних соединений: каждая комбинация совместимых строк сохраняется с той же кратностью, если условия соединения не изменены.
Логически запрос описывает связи между тремя таблицами и фильтр активных клиентов. Физически СУБД может сначала соединить заказы с позициями, затем присоединить клиентов, либо сначала отобрать активных клиентов и только потом искать их заказы. Выбор зависит от статистики, индексов, оценок селективности и стоимости алгоритмов соединения.
Корректность перестановки требует, чтобы предикаты относились к тем же строкам и не меняли семантику при переносе. Нельзя безоговорочно распространять это правило на LEFT JOIN, RIGHT JOIN и FULL JOIN: они сохраняют unmatched-строки, и изменение порядка может изменить наличие NULL-дополненных строк.
Ограничения также возникают при LIMIT или OFFSET, оконных функциях, агрегировании промежуточного результата, коррелированных и латеральных подзапросах, а также у выражений, чьи побочные эффекты или недетерминированность наблюдаемы. На практике оптимизатор применяет только те преобразования, которые считает семантически безопасными для конкретного запроса и диалекта SQL.
В отчёте соединялись крупные таблицы заказов и позиций, после чего результат соединялся с клиентами и фильтровался по статусу клиента. План выполнялся медленно, потому что промежуточно обрабатывалось много заказов неактивных клиентов.
Рассматривались два варианта. Принудительно задавать порядок соединений hints можно, но это делает запрос зависимым от конкретной СУБД и текущего распределения данных. Переписать запрос с дополнительным подзапросом можно, но это не гарантирует физический порядок: оптимизатор вправе снова преобразовать логическое выражение.
Выбрали проверку статистики и индексов, после чего обновили статистику и создали индекс, помогающий быстро находить заказы активных клиентов. СУБД стала выбирать план с ранним сокращением набора клиентов; результат не изменился, а время выполнения уменьшилось. Главный вывод: порядок в тексте запроса не является надёжным способом управления планом, если не используются специально поддерживаемые средства конкретной СУБД.
1. Всегда ли перестановка внутренних соединений сохраняет результат при наличии NULL?
Да, для обычных внутренних соединений — при условии, что сохраняются исходные предикаты и их область действия. Сравнение с NULL даёт UNKNOWN, а строка внутреннего соединения попадает в результат только при истинном условии; при корректной перестановке это правило применяется к тем же парам строк.
Но это не означает, что можно произвольно переносить предикаты между ON и WHERE в запросе с внешними соединениями. Там UNKNOWN и сохранение unmatched-строк могут привести к другому результату.
2. Почему оптимизатор не обязан буквально выполнять соединения слева направо?
Потому что текст SQL выражает декларативное требование к результату. СУБД сначала строит логическое представление запроса, затем выбирает физический план: порядок чтения таблиц, алгоритм соединения и способ применения фильтров.
Порядок слева направо может быть неоптимальным. Например, соединение небольшой отфильтрованной выборки с таблицей по индексу может быть дешевле полного сканирования большой таблицы. Конкретный выбор основан на оценке стоимости, поэтому нет гарантии, что оптимизатор всегда найдёт лучший план.
3. Что изменится, если между соединениями есть LEFT JOIN?
Свобода перестановки становится ограниченной, поскольку LEFT JOIN обязан сохранить каждую строку левой стороны, для которой не найдено совпадение. При изменении порядка таблица, ранее находившаяся справа, может оказаться на стороне, строки которой должны сохраняться.
Например, сначала присоединить позиции к заказам, а затем клиентов обычно можно только при доказанной эквивалентности условий. Но свободно поменять местами левое и внутреннее соединение нельзя: это может удалить или, наоборот, добавить строки с NULL в столбцах правой стороны. Оптимизатор применяет такие преобразования лишь при наличии условий, гарантирующих сохранение результата.