Откуда происходят это постоянное сканирование и левое внешнее соединение в тривиальном плане запроса SELECT?

21

У меня есть эта таблица:

CREATE TABLE [dbo].[Accounts] (
    [AccountId] UNIQUEIDENTIFIER UNIQUE NOT NULL DEFAULT NEWID(),
    -- WHATEVER other columns
);
GO
CREATE UNIQUE CLUSTERED INDEX [AccountsIndex]
    ON [dbo].[Accounts]([AccountId] ASC);
GO

Этот запрос:

DECLARE @result UNIQUEIDENTIFIER
SELECT @result = AccountId FROM Accounts WHERE AccountId='guid-here'

выполняется с планом запроса, состоящим из одного поиска индекса - как и ожидалось:

SELECT <---- Clustered Index Seek

Этот запрос делает то же самое:

DECLARE @result UNIQUEIDENTIFIER
SET @result = (SELECT AccountId FROM Accounts WHERE AccountId='guid-here')

но он выполняется с планом, в котором результат поиска по индексу оставлен на внешнем уровне, соединен с результатом некоторого сканирования констант и затем передан в Compute Scalar:

SELECT <--- Compute Scalar <--- Left Outer Join <--- Constant Scan
                                      ^
                                      |------Clustered Index Seek

Что это за дополнительная магия? Что делает это постоянное сканирование с последующим левым внешним соединением?

Sharptooth
источник

Ответы:

29

Семантика двух утверждений различна:

  • Первый не устанавливает значение переменной, если строка не найдена.
  • Вторая всегда устанавливает переменную, в том числе и ноль, если строка не найдена.

Сканирование констант создает пустую строку (без столбцов!), Которая приведет к обновлению переменной в случае, если ничто не соответствует базовой таблице. Левое объединение гарантирует, что пустая строка сохранится в соединении. Назначение переменной можно рассматривать как происходящее в корневом узле плана выполнения.

С помощью SELECT @result

-- Set initial value
DECLARE @result uniqueidentifier = {guid 'FE2CA909-1162-4C6C-A7AC-33B257E28539'};

-- @result does not change
SELECT @result = AccountId 
FROM Accounts 
WHERE AccountId={guid '7AD4D33C-1ED7-4183-B7F3-48C33D666525'};

SELECT @result;

Результат 1

С помощью SET @result

-- Set initial value
DECLARE @result uniqueidentifier = {guid 'FE2CA909-1162-4C6C-A7AC-33B257E28539'};

-- @result set to null
SET @result = 
(
    SELECT AccountId 
    FROM Accounts 
    WHERE AccountId={guid '7AD4D33C-1ED7-4183-B7F3-48C33D666525'}
);

SELECT @result;

Результат 2

Планы выполнения

ВЫБЕРИТЕ назначениеСтрока не поступает в корневой узел, поэтому назначение не происходит.

SET назначениеСтрока всегда поступает в корневой узел, поэтому происходит присвоение переменной.


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

Существуют и другие способы обеспечения того, чтобы строка генерировалась из подзапроса, чтобы обеспечить присвоение переменной. Одним из них является использование избыточного скалярного агрегата (без группировки по выражению):

-- Set initial value
DECLARE @result uniqueidentifier = {guid 'FE2CA909-1162-4C6C-A7AC-33B257E28539'};

-- @result set to null
SET @result = 
    (
        SELECT MAX(AccountId)
        FROM Accounts 
        WHERE AccountId={guid '7AD4D33C-1ED7-4183-B7F3-48C33D666525'} 
    );
SELECT @result;

Результат 3

Скалярный агрегатный план выполнения

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

Документация:

Если инструкция SELECT не возвращает строк, переменная сохраняет свое текущее значение. Если выражение является скалярным подзапросом, который не возвращает значения, переменная устанавливается в NULL.

Для назначения переменных мы рекомендуем использовать SET @local_variable вместо SELECT @local_variable.

Дальнейшее чтение:

Пол Уайт говорит, что GoFundMonica
источник