Spool de varredura constante

14

Eu tenho uma mesa com algumas dezenas de linhas. A configuração simplificada está seguindo

CREATE TABLE #data ([Id] int, [Status] int);

INSERT INTO #data
VALUES (100, 1), (101, 2), (102, 3), (103, 2);

E eu tenho uma consulta que une esta tabela a um conjunto de linhas construídas com valor de tabela (feitas de variáveis ​​e constantes), como

DECLARE @id1 int = 101, @id2 int = 105;

SELECT
    COALESCE(p.[Code], 'X') AS [Code],
    COALESCE(d.[Status], 0) AS [Status]
FROM (VALUES
        (@id1, 'A'),
        (@id2, 'B')
    ) p([Id], [Code])
    FULL JOIN #data d ON d.[Id] = p.[Id];

O plano de execução da consulta está mostrando que a decisão do otimizador é usar a FULL LOOP JOINestratégia, o que parece apropriado, pois as duas entradas têm muito poucas linhas. Uma coisa que notei (e não posso concordar), porém, é que as linhas do TVC estão sendo colocadas em spool (consulte a área do plano de execução na caixa vermelha).

Spool de varredura constante

Por que o otimizador apresenta o spool aqui, qual é o motivo para fazê-lo? Não há nada complexo além do carretel. Parece que não é necessário. Como se livrar dele, neste caso, quais são as formas possíveis?


O plano acima foi obtido em

Microsoft SQL Server 2014 (SP2-CU11) (KB4077063) - 12.0.5579.0 (X64)

i-one
fonte
Sugestão relacionada em feedback.azure.com
i-one

Respostas:

19

Por que o otimizador apresenta o spool aqui, qual é o motivo para fazê-lo? Não há nada complexo além do carretel.

O que está além do spool não é uma referência simples de tabela, que pode ser simplesmente duplicada quando a junção esquerda / anti-junção esquerda alternativa de for gerada.

Pode parecer um pouco com uma tabela (Constant Scan), mas para o otimizador *, é umaUNION ALL das linhas separadas na VALUEScláusula.

A complexidade adicional é suficiente para o otimizador optar por colocar em spool e repetir as linhas de origem, e não substituir o spool por um simples "table get" mais tarde. Por exemplo, a transformação inicial da junção completa é assim:

plano inicial

Observe os carretéis extras introduzidos pela transformação geral. Carretéis acima de uma tabela simples são limpos posteriormente pela regra SpoolGetToGet.

Se o otimizador tivesse uma SpoolConstGetToConstGetregra correspondente , ele poderia funcionar como você deseja, em princípio.

Como se livrar dele, neste caso, quais são as formas possíveis?

Use uma tabela real (temporária ou variável) ou grave a transformação manualmente da junção completa, por exemplo:

WITH 
    p([Id], [Code]) AS
    (
        SELECT @id1, 'A'
        UNION ALL
        SELECT @id2, 'B'
    ),
    FullJoin AS
    (
        SELECT
            p.Code,
            d.[Status]
        FROM p
        LEFT JOIN #data d 
            ON d.[Id] = p.[Id]
        UNION ALL
        SELECT
            NULL,
            D.[Status]
        FROM #data AS D
        WHERE NOT EXISTS
        (
            SELECT *
            FROM p
            WHERE p.Id = D.Id
        )
    )
SELECT
    COALESCE(FullJoin.Code, 'X') AS Code,
    COALESCE(FullJoin.Status, 0) AS [Status]
FROM FullJoin;

Planeje a reescrita manual:

Plano de reescrita manual

Isso tem um custo estimado de 0,0067201 unidades, em comparação com 0,0203412 unidades para o original.


* Pode ser observado como LogOp_UnionAllna Árvore Convertida (TF 8605). Na Árvore de Entrada (TF 8606), é a LogOp_ConstTableGet. A Árvore Convertida mostra a árvore dos elementos de expressão do otimizador após análise, normalização, algebrização, ligação e algum outro trabalho preparatório. A Árvore de Entrada mostra os elementos após a conversão para o Negation Normal Form (NNF convert), o colapso constante do tempo de execução e alguns outros bits e bobs. A conversão NNF inclui lógica para recolher uniões lógicas e tabelas comuns, entre outras coisas.

Paul White 9
fonte
3

O spool de tabela é simplesmente criar uma tabela dos dois conjuntos de tuplas presentes no VALUES cláusula.

Você pode eliminar o spool inserindo esses valores em uma tabela temporária primeiro, da seguinte maneira:

DROP TABLE IF EXISTS #data;
CREATE TABLE #data ([Id] int, [Status] int);

INSERT INTO #data
VALUES (100, 1), (101, 2), (102, 3), (103, 2);

DROP TABLE IF EXISTS #p;
CREATE TABLE #p
(
    Id int NOT NULL
    , Code char(1) NOT NULL
);

DECLARE @id1 int = 101, @id2 int = 105;

INSERT INTO #p (Id, Code)
VALUES
        (@id1, 'A'),
        (@id2, 'B');


SELECT
    COALESCE(p.[Code], 'X') AS [Code],
    COALESCE(d.[Status], 0) AS [Status]
FROM #p p
    FULL JOIN #data d ON d.[Id] = p.[Id];

Observando o plano de execução da sua consulta, vemos que a lista de saída contém duas colunas que usam o Unionprefixo; esta é uma dica de que o spool está criando uma tabela a partir de uma fonte da união:

insira a descrição da imagem aqui

o FULL OUTER JOIN exige que o SQL Server acesse os valores pduas vezes, uma vez para cada "lado" da associação. Criar um spool permite que os loops internos resultantes se juntem para acessar os dados em spool.

Curiosamente, se você substituir o FULL OUTER JOINpor um LEFT JOINe um RIGHT JOINe UNIONos resultados juntos, o SQL Server não usará um spool.

SELECT
    COALESCE(p.[Code], 'X') AS [Code],
    COALESCE(d.[Status], 0) AS [Status]
FROM (VALUES
        (101, 'A'),
        (105, 'B')
    ) p([Id], [Code])
    LEFT JOIN #data d ON d.[Id] = p.[Id]
UNION
SELECT
    COALESCE(p.[Code], 'X') AS [Code],
    COALESCE(d.[Status], 0) AS [Status]
FROM (VALUES
        (101, 'A'),
        (105, 'B')
    ) p([Id], [Code])
    RIGHT JOIN #data d ON d.[Id] = p.[Id];

insira a descrição da imagem aqui

Observe que não estou sugerindo o uso da UNIONconsulta acima; para conjuntos maiores de entrada, pode não ser mais eficiente do que o simples que FULL OUTER JOINvocê já possui.

Max Vernon
fonte
Na sua carga de trabalho real, o carretel é realmente tão caro?
Max Vernon