De onde vêm esse Constant Scan e Left Outer Join em um plano trivial de consulta SELECT?

21

Eu tenho esta tabela:

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

Esta consulta:

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

executa com um plano de consulta que consiste em uma única busca de índice - conforme o esperado:

SELECT <---- Clustered Index Seek

Esta consulta faz o mesmo:

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

mas é executado com um plano em que o resultado da busca no índice é deixado associado externo com o resultado de algumas varreduras constantes e depois é alimentado no Compute Scalar:

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

O que é essa mágica extra? O que faz essa varredura constante seguida pela junção externa esquerda?

dente afiado
fonte

Respostas:

29

A semântica das duas instruções é diferente:

  • O primeiro não define o valor da variável se nenhuma linha for encontrada.
  • O segundo sempre define a variável, incluindo null se nenhuma linha for encontrada.

A Varredura constante produz uma linha vazia (sem colunas!) Que resultará na atualização da variável caso nada corresponda à tabela base. A junção esquerda garante que a linha vazia sobreviva à junção. A atribuição de variável pode ser pensada como acontecendo no nó raiz do plano de execução.

Usando 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;

Resultado 1

Usando 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;

Resultado 2

Planos de execução

SELECIONAR tarefaNenhuma linha chega ao nó raiz, portanto, nenhuma atribuição ocorre.

Atribuição SETUma linha sempre chega ao nó raiz, portanto ocorre a atribuição de variáveis.


A varredura constante constante e a junção externa esquerda dos loops aninhados não são motivo de preocupação. A junção, em particular, é barata, pois é garantido que você encontre uma linha em sua entrada externa e, no máximo, uma linha (no seu exemplo) na entrada interna.

Existem outras maneiras de garantir que uma linha seja gerada a partir da subconsulta para garantir que uma atribuição de variável ocorra. Uma é usar um agregado escalar redundante (nenhum grupo por cláusula):

-- 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;

Resultado 3

Plano de execução agregado escalar

Observe que o agregado escalar produz uma linha, mesmo que não receba entrada.

Documentação:

Se a instrução SELECT não retornar linhas, a variável manterá seu valor presente. Se expressão for uma subconsulta escalar que não retorna valor, a variável é configurada como NULL.

Para atribuir variáveis, recomendamos que você use SET @local_variable em vez de SELECT @local_variable.

Leitura adicional:

Paul White diz que a GoFundMonica
fonte