Резюме
SQL Server использует правильное соединение (внутреннее или внешнее) и при необходимости добавляет проекции, чтобы учесть всю семантику исходного запроса при выполнении внутренних переводов между apply и join .
Различия в планах можно объяснить различной семантикой агрегатов с предложением group by в SQL Server и без него.
Детали
Присоединиться против Применить
Нам нужно будет различать заявку и объединение :
Подать заявление
Внутренний (нижний) вход применения применяется для каждой строки внешнего (верхнего) ввода, при этом одно или несколько значений параметров внутренней стороны предоставляются текущей внешней строкой. Общий результат применения - это комбинация (объединение всех) всех строк, созданных параметризованными выполнениями внутренней стороны. Наличие параметров означает применение иногда упоминается как коррелированное соединение.
Применяются всегда реализуется в планах выполнения до вложенных циклов оператора. Оператор будет иметь свойство Outer References, а не предикаты соединения. Внешние ссылки - это параметры, передаваемые с внешней стороны на внутреннюю сторону на каждой итерации цикла.
Присоединиться
Объединение оценивает его предикат объединения в операторе соединения. Как правило, объединение может быть реализовано с помощью операторов Hash Match , Merge или Nested Loops в SQL Server.
При выборе вложенных циклов его можно отличить от применения по отсутствию внешних ссылок (и, как правило, наличию предиката соединения). Внутренний вход объединения никогда не ссылается на значения из внешнего ввода - внутренняя сторона все еще выполняется один раз для каждой внешней строки, но выполнение внутренней стороны не зависит от каких-либо значений из текущей внешней строки.
Для получения более подробной информации см. Мой пост « Применить против вложенных циклов» .
... почему в плане выполнения есть внешнее соединение, а не внутреннее соединение?
Внешнее объединение возникает, когда оптимизатор преобразует заявку в объединение (используя вызываемое правило ApplyHandler
), чтобы посмотреть, сможет ли он найти более дешевый план на основе объединения. Объединение должно быть внешним объединением для корректности, когда приложение содержит скалярный агрегат . Внутреннее соединение не будет гарантированно производить те же результаты , как в оригинале применяются , как мы увидим.
Скалярные и векторные агрегаты
- Агрегат без соответствующего
GROUP BY
предложения является скалярным агрегатом.
- Агрегат с соответствующим
GROUP BY
предложением является векторным агрегатом.
В SQL Server скалярное агрегирование всегда создает строку, даже если ей не дано ни одной строки для агрегирования. Например, скалярный COUNT
агрегат без строк равен нулю. Вектор COUNT
совокупность каких - либо строк пустое множество (ни одной строки на всех).
Следующие запросы игрушек иллюстрируют разницу. Вы также можете прочитать больше о скалярных и векторных агрегатах в моей статье Fun with Scalar and Vector Aggregates .
-- Produces a single zero value
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1;
-- Produces no rows
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1 GROUP BY ();
db <> Fiddle demo
Преобразование применить, чтобы присоединиться
Я упоминал ранее, что соединение должно быть внешним соединением для корректности, когда исходное применение содержит скалярный агрегат . Чтобы показать, почему это так, я буду использовать упрощенный пример запроса вопроса:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
SELECT * FROM @A AS A
CROSS APPLY (SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A) AS CA;
Правильный результат для столбца c
равен нулю , потому что COUNT_BIG
это скалярный агрегат. При переводе этого запроса на применение в форму соединения SQL Server создает внутреннюю альтернативу, которая выглядела бы следующим образом, если бы она была выражена в T-SQL:
SELECT A.*, c = COALESCE(J1.c, 0)
FROM @A AS A
LEFT JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
Чтобы переписать приложение как некоррелированное соединение, мы должны ввести GROUP BY
в производную таблицу (иначе не может быть A
столбца, к которому можно присоединиться). Соединение должно быть внешним соединением, поэтому каждая строка таблицы @A
продолжает генерировать строку в выходных данных. При левом соединении создается NULL
столбец for, c
когда предикат объединения не оценивается как true. Это NULL
должно быть переведено в ноль, COALESCE
чтобы завершить правильное преобразование из применения .
Демонстрация ниже показывает, как внешнее объединение и COALESCE
как оно требуется для получения одинаковых результатов с использованием объединения в качестве исходного запроса на применение :
db <> Fiddle demo
С GROUP BY
... почему раскомментирование предложения group by приводит к внутреннему объединению?
Продолжая упрощенный пример, но добавив GROUP BY
:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
-- Original
SELECT * FROM @A AS A
CROSS APPLY
(SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A GROUP BY B.A) AS CA;
COUNT_BIG
Теперь вектор совокупности, так что правильный результат для пустого входного набора не больше не равна нулю, то нет ни в одной строке вообще . Другими словами, выполнение приведенных выше операторов не приводит к выводу.
Эту семантику гораздо проще соблюдать при переводе из применения в соединение , поскольку CROSS APPLY
естественным образом отклоняется любая внешняя строка, которая не создает внутренних боковых строк. Поэтому теперь мы можем безопасно использовать внутреннее соединение, без дополнительной проекции выражения:
-- Rewrite
SELECT A.*, J1.c
FROM @A AS A
JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
Приведенная ниже демонстрация показывает, что перезапись внутреннего соединения приводит к тем же результатам, что и исходное применение с векторным агрегатом:
db <> Fiddle demo
Оптимизатор выбирает внутреннее объединение слиянием с маленькой таблицей, потому что он быстро находит дешевый план соединения (найден достаточно хороший план). Оптимизатор, основанный на затратах, может переписать объединение обратно к заявке - возможно, найдет более дешевый план применения, как это будет здесь, если используется подсказка циклического соединения или принудительный поиск - но в этом случае усилия не стоят.
Примечания
В упрощенных примерах используются разные таблицы с различным содержанием, чтобы более четко показать семантические различия.
Можно утверждать, что оптимизатор должен иметь возможность рассуждать о том, что самосоединение не способно генерировать какие-либо несовпадающие (несоединяющиеся) строки, но сегодня он не содержит такой логики. Многократный доступ к одной и той же таблице в запросе не всегда дает одинаковые результаты в зависимости от уровня изоляции и одновременной активности.
Оптимизатор беспокоится об этой семантике и крайних случаях, поэтому вам не нужно этого делать.
Бонус: Inner Apply Plan
SQL Server может создать внутренний план применения (не внутренний план соединения !) Для примера запроса, он просто выбирает не по соображениям стоимости. Стоимость плана внешнего соединения, показанного в вопросе, составляет 0,02898 единиц на экземпляре SQL Server 2017 моего ноутбука.
Вы можете принудительно применить план (коррелированное соединение), используя недокументированный и неподдерживаемый флаг трассировки 9114 (который отключает ApplyHandler
и т. Д.) Только для иллюстрации:
SELECT *
FROM #MyTable AS mt
CROSS APPLY
(
SELECT COUNT_BIG(DISTINCT mt2.Col_B) AS dc
FROM #MyTable AS mt2
WHERE mt2.Col_A = mt.Col_A
--GROUP BY mt2.Col_A
) AS ca
OPTION (QUERYTRACEON 9114);
Это создает план применения вложенных циклов с ленивой индексной шпулей. Общая сметная стоимость составляет 0,0463983 (выше, чем выбранный план):
Обратите внимание , что план выполнения , используя применять вложенные циклы производит правильные результаты с помощью «внутреннего соединения» семантики независимо от наличия GROUP BY
оговорки.
В реальном мире у нас обычно был бы индекс для поддержки поиска на внутренней стороне заявки, чтобы поощрять SQL Server к естественному выбору этой опции, например:
CREATE INDEX i ON #MyTable (Col_A, Col_B);
db <> Fiddle demo