Por que esse CTE recursivo com um parâmetro não usa um índice quando usa um literal?

8

Estou usando um CTE recursivo em uma estrutura em árvore para listar todos os descendentes de um nó específico na árvore. Se eu escrever um valor nó literal no meu WHEREcláusula, o SQL Server parece realmente aplicar o CTE apenas a esse valor, dando um plano de consulta com baixas contagens linhas reais, et cetera :

plano de consulta com valor literal

No entanto, se eu passar o valor como parâmetro, ele parece realizar (spool) o CTE e filtrá-lo após o fato :

plano de consulta com valor do parâmetro

Eu poderia estar lendo os planos errado. Não notei um problema de desempenho, mas estou preocupado que a realização do CTE possa causar problemas com conjuntos de dados maiores, especialmente em um sistema mais ocupado. Além disso, eu normalmente componho essa travessia em si mesma: vou até os ancestrais e volto aos descendentes (para garantir que reuno todos os nós relacionados). Devido à forma como meus dados são, cada conjunto de nós "relacionados" é bastante pequeno, portanto a realização do CTE não faz sentido. E quando o SQL Server parece realizar o CTE, ele está me dando alguns números bastante grandes em suas contagens "reais".

Existe uma maneira de obter a versão parametrizada da consulta para agir como a versão literal? Quero colocar o CTE em uma exibição reutilizável.

Consulta com literal:

CREATE PROCEDURE #c AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = 24
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c;

Consulta com o parâmetro:

CREATE PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c 24;

Código de instalação:

DECLARE @count BIGINT = 100000;
CREATE TABLE #tree (
     Id BIGINT NOT NULL PRIMARY KEY
    ,ParentId BIGINT
);
CREATE INDEX tree_23lk4j23lk4j ON #tree (ParentId);
WITH number AS (SELECT
         CAST(1 AS BIGINT) Value
    UNION ALL SELECT
         n.Value * 2 + 1
    FROM number n
    WHERE n.Value * 2 + 1 <= @count
    UNION ALL SELECT
         n.Value * 2
    FROM number n
    WHERE n.Value * 2 <= @count)
INSERT #tree (Id, ParentId)
SELECT n.Value, CASE WHEN n.Value % 3 = 0 THEN n.Value / 4 END
FROM number n;
binki
fonte

Respostas:

12

A resposta de Randi Vertongen aborda corretamente como você pode obter o plano desejado com a versão parametrizada da consulta. Esta resposta complementa que, abordando o título da pergunta, caso você esteja interessado nos detalhes.

O SQL Server reescreve CTEs (expressões de tabela comum) recursivas de cauda como iteração. Tudo, desde o Lazy Index Spool down, é a implementação em tempo de execução da tradução iterativa. Escrevi um relato detalhado de como esta seção de um plano de execução funciona em resposta ao uso de EXCEPT em uma expressão de tabela comum recursiva .

Você deseja especificar um predicado (filtro) fora do CTE e pedir ao otimizador de consulta que empurre esse filtro para baixo dentro da recursão (reescrita como iteração) e aplique ao membro âncora. Isso significa que a recursão começa apenas com os registros correspondentes ParentId = @Id.

Essa é uma expectativa bastante razoável, seja usado um valor literal, variável ou parâmetro; no entanto, o otimizador pode fazer apenas coisas para as quais as regras foram escritas. As regras especificam como uma árvore de consultas lógicas é modificada para obter uma transformação específica. Eles incluem lógica para garantir que o resultado final seja seguro - ou seja, ele retorna exatamente os mesmos dados que a especificação de consulta original em todos os casos possíveis.

A regra responsável por enviar predicados em uma CTE recursiva é chamada SelOnIterator- uma seleção relacional (= predicado) em um iterador que implementa recursão. Mais precisamente, essa regra pode copiar uma seleção para a parte âncora da iteração recursiva:

Sel(Iter(A,R)) -> Sel(Iter(Sel(A),R))

Esta regra pode ser desativada com a dica não documentada OPTION(QUERYRULEOFF SelOnIterator). Quando isso é usado, o otimizador não pode mais enviar predicados com um valor literal até a âncora de um CTE recursivo. Você não quer isso, mas ilustra o ponto.

Originalmente, essa regra estava limitada ao trabalho em predicados apenas com valores literais. Também pode ser feito para trabalhar com variáveis ​​ou parâmetros, especificando OPTION (RECOMPILE), uma vez que essa dica ativa a Otimização de Incorporação de Parâmetro , na qual o valor literal de tempo de execução da variável (ou parâmetro) é usado ao compilar o plano. O plano não é armazenado em cache; portanto, a desvantagem é uma compilação nova em cada execução.

Em algum momento, a SelOnIteratorregra foi aprimorada para também trabalhar com variáveis ​​e parâmetros. Para evitar alterações inesperadas no plano, isso foi protegido no sinalizador de rastreamento 4199, no nível de compatibilidade do banco de dados e no nível de compatibilidade do hotfix do otimizador de consultas. Esse é um padrão bastante normal para aprimoramentos do otimizador, que nem sempre são documentados. As melhorias são normalmente boas para a maioria das pessoas, mas sempre há uma chance de que qualquer alteração introduza uma regressão para alguém.

Quero colocar o CTE em uma exibição reutilizável

Você pode usar uma função com valor de tabela embutido em vez de uma exibição. Forneça o valor que você deseja empurrar para baixo como parâmetro e coloque o predicado no membro âncora recursivo.

Se preferir, ativar o sinalizador de rastreamento 4199 globalmente também é uma opção. Há muitas alterações no otimizador cobertas por esse sinalizador, portanto, você deve testar cuidadosamente sua carga de trabalho com ela ativada e estar preparado para lidar com as regressões.

Paul White 9
fonte
10

Embora no momento eu não tenha o título do hotfix real, o melhor plano de consulta será usado ao habilitar os hotfixes do otimizador de consultas em sua versão (SQL Server 2012).

Alguns outros métodos são:

  • Usando OPTION(RECOMPILE)para que a filtragem ocorra anteriormente, no valor literal.
  • No SQL Server 2016 ou superior, os hotfixes anteriores a esta versão são aplicados automaticamente e a consulta também deve ser equivalente ao melhor plano de execução.

Hotfixes do otimizador de consulta

Você pode ativar essas correções com

  • Traceflag 4199 antes do SQL Server 2016
  • ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES=ON; a partir do SQL Server 2016. (não é necessário para sua correção)

A filtragem @idativada é aplicada anteriormente aos membros recursivo e âncora no plano de execução com o hotfix ativado.

O traceflag pode ser adicionado no nível da consulta:

OPTION(QUERYTRACEON 4199)

Ao executar a consulta no SQL Server 2012 SP4 GDR ou SQL Server 2014 SP3 com Traceflag 4199, o melhor plano de consulta é escolhido:

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
    OPTION( QUERYTRACEON 4199 );

END;
GO
EXEC #c 24;

Plano de consulta no SQL Server 2014 SP3 com traceflag 4199

Plano de consulta no SQL Server 2012 SP4 GDR com traceflag 4199

Plano de consulta no SQL Server 2012 SP4 GDR sem traceflag 4199

O principal consenso é ativar o traceflag 4199 globalmente ao usar uma versão anterior ao SQL Server 2016. Posteriormente, é aberto o debate para habilitá-lo ou não. AQ / A nisso aqui .


Nível de compatibilidade 130 ou 140

Ao testar a consulta parametrizada em um banco de dados com compatibility_level= 130 ou 140, a filtragem ocorre anteriormente:

insira a descrição da imagem aqui

Devido ao fato de as correções 'antigas' do traceflag 4199 serem ativadas no SQL Server 2016 e superior.


OPÇÃO (RECOMPLE)

Mesmo que um procedimento seja usado, o SQL Server poderá filtrar o valor literal ao adicionar OPTION(RECOMPILE);.

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
OPTION(
RECOMPILE )

END;
GO

insira a descrição da imagem aqui

Plano de consulta no SQL Server 2012 SP4 GDR com OPTION (RECOMPILE)

Randi Vertongen
fonte