CROSS APPLY производит внешнее соединение

17

В ответ на подсчет SQL в разных разделах Эрик Дарлинг опубликовал этот код, чтобы обойти его из-за отсутствия COUNT(DISTINCT) OVER ():

SELECT      *
FROM        #MyTable AS mt
CROSS APPLY (   SELECT COUNT(DISTINCT mt2.Col_B) AS dc
                FROM   #MyTable AS mt2
                WHERE  mt2.Col_A = mt.Col_A
                -- GROUP BY mt2.Col_A 
            ) AS ca;

В запросе используется CROSS APPLY(не OUTER APPLY), так почему в плане выполнения существует внешнее соединение, а не внутреннее ?

введите описание изображения здесь

Кроме того, почему раскомментирование предложения group by приводит к внутреннему соединению?

введите описание изображения здесь

Я не думаю, что данные важны, но копирование с того, что дал Кевин, что по другому вопросу:

create table #MyTable (
Col_A varchar(5),
Col_B int
)

insert into #MyTable values ('A',1)
insert into #MyTable values ('A',1)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',3)

insert into #MyTable values ('B',4)
insert into #MyTable values ('B',4)
insert into #MyTable values ('B',5)
Пол Уайт восстановил Монику
источник

Ответы:

23

Резюме

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 (выше, чем выбранный план):

Index Spool применить план

Обратите внимание , что план выполнения , используя применять вложенные циклы производит правильные результаты с помощью «внутреннего соединения» семантики независимо от наличия GROUP BYоговорки.

В реальном мире у нас обычно был бы индекс для поддержки поиска на внутренней стороне заявки, чтобы поощрять SQL Server к естественному выбору этой опции, например:

CREATE INDEX i ON #MyTable (Col_A, Col_B);

db <> Fiddle demo

Пол Уайт восстановил Монику
источник
-3

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

Оператора физического применения нет, и SQL Server переводит его в подходящий и, надеюсь, эффективный оператор соединения.

Вы можете найти список физических операторов по ссылке ниже.

https://docs.microsoft.com/en-us/sql/relational-databases/showplan-logical-and-physical-operators-reference?view=sql-server-2017

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

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

edit / Кажется, я неправильно понял ваш вопрос. SQL-сервер обычно выбирает наиболее подходящий оператор. Ваш запрос не должен возвращать значения для всех комбинаций обеих таблиц, когда используется перекрестное соединение. Достаточно просто вычислить значение, которое вы хотите для каждой строки, что и делается здесь.

Дж. Мэйс
источник