Por que o custo estimado de (o mesmo) 1.000 buscas em um índice único difere nesses planos?

27

Nas consultas abaixo, estima-se que ambos os planos de execução executem 1.000 buscas em um índice exclusivo.

As buscas são conduzidas por uma varredura ordenada na mesma tabela de origem, portanto, aparentemente, eles devem procurar os mesmos valores na mesma ordem.

Os dois loops aninhados têm <NestedLoops Optimized="false" WithOrderedPrefetch="true">

Alguém sabe por que essa tarefa custa 0,172434 no primeiro plano, mas 3,01702 no segundo?

(O motivo da pergunta é que a primeira consulta foi sugerida para mim como uma otimização devido ao aparente custo do plano muito mais baixo. Na verdade, parece-me que faz mais trabalho, mas estou apenas tentando explicar a discrepância. .)

Configuração

CREATE TABLE dbo.Target(KeyCol int PRIMARY KEY, OtherCol char(32) NOT NULL);

CREATE TABLE dbo.Staging(KeyCol int PRIMARY KEY, OtherCol char(32) NOT NULL); 

INSERT INTO dbo.Target
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY @@SPID), LEFT(NEWID(),32)
FROM master..spt_values v1,  
     master..spt_values v2;

INSERT INTO dbo.Staging
SELECT TOP (1000) ROW_NUMBER() OVER (ORDER BY @@SPID), LEFT(NEWID(),32)
FROM master..spt_values v1;

Consulta 1 link "Colar o plano"

WITH T
     AS (SELECT *
         FROM   Target AS T
         WHERE  T.KeyCol IN (SELECT S.KeyCol
                             FROM   Staging AS S))
MERGE T
USING Staging S
ON ( T.KeyCol = S.KeyCol )
WHEN NOT MATCHED THEN
  INSERT ( KeyCol, OtherCol )
  VALUES(S.KeyCol, S.OtherCol )
WHEN MATCHED AND T.OtherCol > S.OtherCol THEN
  UPDATE SET T.OtherCol = S.OtherCol;

Consulta 2: link "Colar o plano"

MERGE Target T
USING Staging S
ON ( T.KeyCol = S.KeyCol )
WHEN NOT MATCHED THEN
  INSERT ( KeyCol, OtherCol )
  VALUES( S.KeyCol, S.OtherCol )
WHEN MATCHED AND T.OtherCol > S.OtherCol THEN
  UPDATE SET T.OtherCol = S.OtherCol; 

Consulta 1

Consulta 2

O acima foi testado no SQL Server 2014 (SP2) (KB3171021) - 12.0.5000.0 (X64)


@Joe Obbish aponta nos comentários que uma reprodução mais simples seria

SELECT *
FROM staging AS S 
  LEFT OUTER JOIN Target AS T 
    ON T.KeyCol = S.KeyCol;

vs

SELECT *
FROM staging AS S 
  LEFT OUTER JOIN (SELECT * FROM Target) AS T 
    ON T.KeyCol = S.KeyCol;

Para a tabela intermediária de 1.000 linhas, as duas opções anteriores ainda têm a mesma forma do plano com loops aninhados e o plano sem a tabela derivada parecer mais barata, mas para uma tabela intermediária de 10.000 linhas e a mesma tabela de destino acima da diferença de custos, o plano é alterado O formato (com uma junção de varredura e mesclagem completa parecendo relativamente mais atraente do que as buscas caras) mostra essa discrepância de custo pode ter implicações além de dificultar a comparação de planos.

insira a descrição da imagem aqui

Martin Smith
fonte

Respostas:

20

Alguém sabe por que essa tarefa custa 0,172434 no primeiro plano, mas 3,01702 no segundo?

De um modo geral, uma busca do lado interno abaixo de uma junção de loops aninhados é cara, assumindo um padrão de E / S aleatório. Há uma redução simples baseada em substituição para acessos subseqüentes, representando a chance de a página necessária já ter sido trazida para a memória por uma iteração anterior. Essa avaliação básica produz o custo padrão (mais alto).

Há outra entrada de custo, o Smart Seek Costing , sobre o qual poucos detalhes são conhecidos. Meu palpite (e isso é tudo o que é nesta fase) é que o SSC tenta avaliar o custo de E / S de busca interna com mais detalhes, talvez considerando a ordem local e / ou a faixa de valores a serem buscados. Quem sabe.

Por exemplo, a primeira operação de busca traz não apenas a linha solicitada, mas todas as linhas nessa página (em ordem de índice). Dado o padrão geral de acesso, buscar 1000 linhas em 1000 buscas requer apenas 2 leituras físicas, mesmo com a leitura antecipada e a pré-busca desativadas. Nessa perspectiva, o custo de E / S padrão representa uma superestimação significativa e o custo ajustado pelo SSC está mais próximo da realidade.

Parece razoável esperar que o SSC seja mais eficaz quando o loop direciona um índice a buscar mais ou menos diretamente, e a referência externa da junção é a base da operação de busca. Pelo que sei, o SSC é sempre tentado para operações físicas adequadas, mas na maioria das vezes não produz um ajuste descendente quando a busca é separada da junção por outras operações. Os filtros simples são uma exceção a isso, talvez porque o SQL Server possa frequentemente enviá-los para o operador de acesso a dados. De qualquer forma, o otimizador possui um suporte bastante profundo para seleções.

É lamentável que o Scalar de computação para as projeções externas da subconsulta pareça interferir no SSC aqui. Os escalares de computação geralmente são realocados acima da junção, mas esses precisam permanecer onde estão. Mesmo assim, a maioria dos escalares de computação normais são bastante transparentes à otimização, então isso é um pouco surpreendente.

Independentemente disso, quando a operação física PhyOp_Rangeé produzida a partir de uma seleção simples em um índice SelIdxToRng, o SSC é eficaz. Quando o mais complexo SelToIdxStrategy(seleção em uma tabela para uma estratégia de índice) é empregado, o resultado PhyOp_Rangeexecuta o SSC, mas não resulta em redução. Novamente, parece que operações mais simples e diretas funcionam melhor com o SSC.

Eu gostaria de poder dizer exatamente o que o SSC faz e mostrar os cálculos exatos, mas não conheço esses detalhes. Se você quiser explorar a saída de rastreio limitada disponível para si, poderá empregar o sinalizador de rastreio não documentado 2398. Um exemplo de saída é:

Custeio de busca inteligente (7.1) :: 1.34078e + 154, 0.001

Esse exemplo refere-se ao grupo de notas 7, alternativa 1, mostrando um limite superior de custo e um fator de 0,001. Para ver fatores mais limpos, reconstrua as tabelas sem paralelismo para que as páginas fiquem o mais densas possível. Sem fazer isso, o fator é mais parecido com 0,000821 para sua tabela de destino de exemplo. Existem algumas relações bastante óbvias lá, é claro.

O SSC também pode ser desativado com o sinalizador de rastreamento não documentado 2399. Com esse sinalizador ativo, os dois custos têm o valor mais alto.

Paul White diz que a GoFundMonica
fonte
8

Não tenho certeza se é uma resposta, mas é um pouco longo para um comentário. A causa da diferença é pura especulação da minha parte e talvez possa servir de reflexão para os outros.

Consultas simplificadas com planos de execução.

SELECT S.KeyCol, 
       S.OtherCol,
       T.*
FROM staging AS S 
  LEFT OUTER JOIN Target AS T 
    ON T.KeyCol = S.KeyCol;

SELECT S.KeyCol, 
       S.OtherCol,
       T.*
FROM staging AS S 
  LEFT OUTER JOIN (
                  SELECT *
                  FROM Target
                  ) AS T 
    ON T.KeyCol = S.KeyCol;

insira a descrição da imagem aqui

A principal diferença entre essas consultas equivalentes que realmente podem resultar em planos de execução idênticos é o operador escalar de computação. Não sei por que ele deve estar lá, mas acho que é o máximo que o otimizador pode otimizar a tabela derivada.

Meu palpite é que a presença do escalar de computação é o que está diminuindo o custo de IO para a segunda consulta.

De dentro do otimizador: planejar custos

O custo da CPU é calculado como 0,0001581 para a primeira linha e 0,000011 para as linhas subseqüentes.
...
O custo de E / S de 0,003125 é exatamente 1/320 - refletindo a suposição do modelo de que o subsistema de disco pode executar 320 operações aleatórias de E / S por segundo
...
o componente de custo é inteligente o suficiente para reconhecer que o número total de as páginas que precisam ser trazidas do disco nunca podem exceder o número de páginas necessárias para armazenar a tabela inteira.

No meu caso, a tabela ocupa 5618 páginas e, para obter 1000 linhas de 1000000, o número estimado de páginas necessárias é 5.618, fornecendo o custo de IO de 0,015625.

O custo da CPU para as duas consultas é o mesmo 0.0001581 * 1000 executions = 0.1581,.

Portanto, de acordo com o artigo acima, podemos calcular o custo para a primeira consulta como 0,173725.

E supondo que eu esteja certo sobre como o escalar de computação está atrapalhando o custo de IO, ele pode ser calculado para 3,2831.

Não é exatamente o que é mostrado nos planos, mas está bem ali no bairro.

Mikael Eriksson
fonte
6

(Isso seria melhor como comentário à resposta de Paul, mas ainda não tenho representante suficiente.)

Eu queria fornecer a lista de sinalizadores de rastreamento (e algumas DBCCdeclarações) que eu chegava quase a uma conclusão, caso seja útil investigar discrepâncias semelhantes no futuro. Tudo isso não deve ser usado na produção .

Primeiro, dei uma olhada no Memorando Final para ver quais operadores físicos estavam sendo usados. Eles certamente têm a mesma aparência, de acordo com os planos gráficos de execução. Então, usei sinalizadores de rastreamento 3604e 8615, o primeiro direciona a saída para o cliente e o segundo revela o Memorando Final:

SELECT S.*, T.KeyCol
FROM Staging AS S
      LEFT OUTER JOIN Target AS T
       ON T.KeyCol = S.KeyCol
OPTION(QUERYTRACEON 3604, -- Output client info
       QUERYTRACEON 8615, -- Shows Final Memo structure
       RECOMPILE);

Partindo do Root Group, encontrei esses PhyOp_Rangeoperadores quase idênticos :

  1. PhyOp_Range 1 ASC 2.0 Cost(RowGoal 0,ReW 0,ReB 999,Dist 1000,Total 1000)= 0.175559(Distance = 2)
  2. PhyOp_Range 1 ASC 3.0 Cost(RowGoal 0,ReW 0,ReB 999,Dist 1000,Total 1000)= 3.01702(Distance = 2)

A única diferença óbvia para mim foi o 2.0e 3.0, que se refere ao respectivo "grupo de notas 2, original" e "grupo de notas 3, original". Verificando o memorando, eles se referem à mesma coisa - portanto, nenhuma diferença foi revelada ainda.

Segundo, examinei toda uma bagunça de sinalizadores de rastreamento que se mostraram infrutíferos para mim - mas têm algum conteúdo interessante. Eu levantei mais de Benjamin Nevarez . Eu estava procurando pistas sobre as regras de otimização que foram aplicadas em um caso e não no outro.

 SELECT S.*, T.KeyCol
 FROM Staging AS S
      LEFT OUTER JOIN Target AS T
        ON T.KeyCol = S.KeyCol
 OPTION (QUERYTRACEON 3604, -- Output info to client
         QUERYTRACEON 2363, -- Show stats and cardinality info
         QUERYTRACEON 8675, -- Show optimization process info
         QUERYTRACEON 8606, -- Show logical query trees
         QUERYTRACEON 8607, -- Show physical query tree
         QUERYTRACEON 2372, -- Show memory utilization info for optimization stages 
         QUERYTRACEON 2373, -- Show memory utilization info for applying rules
         RECOMPILE );

Terceiro, observei quais regras foram aplicadas para os nossos PhyOp_Ranges que são tão semelhantes. Eu usei alguns sinalizadores de rastreamento mencionados por Paul em uma postagem no blog .

SELECT S.*, T.KeyCol
FROM Staging AS S
      LEFT OUTER JOIN (SELECT KeyCol
                      FROM Target) AS T
       ON T.KeyCol = S.KeyCol
OPTION (QUERYTRACEON 3604, -- Output info to client
        QUERYTRACEON 8619, -- Show applied optimization rules
        QUERYTRACEON 8620, -- Show rule-to-memo info
        QUERYTRACEON 8621, -- Show resulting tree
        QUERYTRACEON 2398, -- Show "smart seek costing"
        RECOMPILE );

A partir do resultado, vemos que o direct- JOINaplicada esta regra para obter o nosso PhyOp_Rangeoperador: Rule Result: group=7 2 <SelIdxToRng>PhyOp_Range 1 ASC 2 (Distance = 2). O subselect aplicada esta regra em vez disso: Rule Result: group=9 2 <SelToIdxStrategy>PhyOp_Range 1 ASC 3 (Distance = 2). Também é aqui que você vê as informações de "custo de busca inteligente" associadas a cada regra. Para o direct- JOINesta é a saída (para mim): Smart seek costing (7.2) :: 1.34078e+154 , 0.001. Para o subselect, esta é a saída: Smart seek costing (9.2) :: 1.34078e+154 , 1.

No final, não pude concluir muito - mas a resposta de Paul fecha a maior parte da lacuna. Gostaria de ver mais algumas informações sobre o custo de busca inteligente.

Steven Hibble
fonte
4

Isso também não é realmente uma resposta - como observou Mikael, é difícil discutir esse problema nos comentários ...

Curiosamente, se você converter a subconsulta (select KeyCol FROM Target)em um TVF embutido, verá o plano e seus custos iguais aos da consulta original simples:

CREATE FUNCTION dbo.cs_test()
RETURNS TABLE
WITH SCHEMABINDING
AS 
RETURN (
    SELECT KeyCol FROM dbo.Target
    );

/* "normal" variant */
SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN Target AS T ON T.KeyCol = S.KeyCol;

/* "subquery" variant */
SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN (SELECT KeyCol FROM Target) AS T ON T.KeyCol = S.KeyCol;

/* "inline-TVF" variant */
SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN dbo.cs_test() t ON s.KeyCol = t.Keycol

Os planos de consulta ( cole o link do plano ):

insira a descrição da imagem aqui

A dedução me leva a acreditar que o mecanismo de custo está confuso sobre o impacto potencial que esse tipo de subconsulta pode ter .

Tomemos, por exemplo, o seguinte:

SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN (
        SELECT KeyCol = CHECKSUM(NEWID()) 
        FROM Target
        ) AS T ON T.KeyCol = S.KeyCol;

Como você custaria isso? O otimizador de consulta escolhe um plano muito semelhante à variante "subconsulta" acima, contendo um escalar de computação ( link pastetheplan.com ):

insira a descrição da imagem aqui

O escalar de computação tem um custo bastante diferente da variante "subconsulta" mostrada acima, no entanto, ainda é apenas um palpite, pois o otimizador de consultas não tem como saber, a priori, qual pode ser o número de linhas retornadas. O plano usa uma correspondência de hash para a junção externa esquerda, pois as estimativas de linha são desconhecidas e, portanto, configuradas para o número de linhas na tabela Destino.

insira a descrição da imagem aqui

Não tenho uma grande conclusão disso, exceto que concordo com o trabalho que Mikael fez em sua resposta e espero que alguém possa encontrar uma resposta melhor.

Max Vernon
fonte