A inclusão de ORDER BY na consulta que não retorna linhas afeta drasticamente o desempenho

15

Dada uma associação simples de três tabelas, o desempenho da consulta muda drasticamente quando ORDER BY é incluído, mesmo sem nenhuma linha retornada. O cenário real do problema leva 30 segundos para retornar zero linhas, mas é instantâneo quando ORDER BY não está incluído. Por quê?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */

Entendo que eu poderia ter um índice em bigtable.smallGuidId, mas acredito que isso realmente pioraria as coisas nesse caso.

Aqui está o script para criar / preencher as tabelas para teste. Curiosamente, parece importar que smalltable tenha um campo nvarchar (max). Também parece importar que eu estou entrando na bigtable com um guid (o que eu acho que faz com que ele queira usar a correspondência de hash).

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 

Eu testei no SQL 2005, 2008 e 2008R2 com os mesmos resultados.

Hafthor
fonte

Respostas:

32

Concordo com a resposta de Martin Smith, mas o problema não é simplesmente uma estatística, exatamente. As estatísticas da coluna ForeignId (supondo que as estatísticas automáticas estejam ativadas) mostram com precisão que não existem linhas com o valor 3 (há apenas uma, com o valor 7):

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

saída de estatísticas

O SQL Server sabe que as coisas podem ter mudado desde que as estatísticas foram capturadas; portanto, pode haver uma linha para o valor 3 quando o plano é executado . Além disso, qualquer período de tempo pode decorrer entre a compilação e a execução do plano (afinal, os planos são armazenados em cache para reutilização). Como Martin diz, o SQL Server contém lógica para detectar quando foram feitas modificações suficientes para justificar a recompilação de qualquer plano em cache por motivos de otimização.

Nada disso importa, no entanto. Com uma exceção de aresta, o otimizador nunca estimará o número de linhas produzidas por uma operação de tabela como zero. Se ele pode determinar estaticamente que a saída deve sempre ter zero linhas, a operação é redundante e será removida completamente.

O modelo do otimizador, em vez disso, estima um mínimo de uma linha. Empregar essa heurística tende a produzir melhores planos em média do que seria o caso se uma estimativa mais baixa fosse possível. Um plano que produz uma estimativa de linha zero em algum estágio seria inútil a partir desse ponto no fluxo de processamento, pois não haveria base para tomar decisões baseadas em custo (linhas zero são zero linhas, não importa o que). Se a estimativa estiver incorreta, a forma do plano acima da estimativa de linha zero quase não tem chance de ser razoável.

O segundo fator é outra suposição de modelagem chamada Suposição de Contenção. Essencialmente, isso diz que se uma consulta associa um intervalo de valores a outro intervalo de valores, é porque os intervalos se sobrepõem. Outra maneira de colocar isso é dizer que a junção está sendo especificada porque espera-se que as linhas sejam retornadas. Sem esse raciocínio, os custos seriam geralmente subestimados, resultando em planos inadequados para uma ampla gama de consultas comuns.

Essencialmente, o que você tem aqui é uma consulta que não se encaixa no modelo do otimizador. Não há nada que possamos fazer para "melhorar" as estimativas com índices filtrados ou com várias colunas; não há como obter uma estimativa menor que uma linha aqui. Um banco de dados real pode ter chaves estrangeiras para garantir que essa situação não ocorra, mas, assumindo que não é aplicável aqui, resta dicas para corrigir a condição fora de modelo. Qualquer número de abordagens de dicas diferentes funcionará com essa consulta. OPTION (FORCE ORDER)é aquele que funciona bem com a consulta conforme escrita.

Paul White restabelece Monica
fonte
21

O problema básico aqui é uma estatística.

Para ambas as consultas, a contagem estimada de linhas mostra que ela acredita que a final SELECTretornará 1.048.580 linhas (o mesmo número de linhas estimadas em existir bigtable) em vez dos 0 que realmente se seguem.

Suas JOINcondições correspondem e preservam todas as linhas. Eles acabam sendo eliminados porque a linha única tinytablenão corresponde ao t.foreignId=3predicado.

Se você correr

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

e olhar para o número estimado de linhas é 1mais do que 0e isso propaga erro em todo o plano. tinytableatualmente contém 1 linha. As estatísticas não serão recompiladas para esta tabela até que ocorram 500 modificações de linha , para que uma linha correspondente possa ser adicionada e não acionar uma recompilação.

A razão pela qual a Ordem de Ingresso é alterada quando você adiciona a ORDER BYcláusula e existe uma varchar(max)coluna smalltableé porque ela estima que as varchar(max)colunas aumentarão o tamanho da linha em 4.000 bytes, em média. Multiplique isso por 1048580 linhas e isso significa que a operação de classificação precisaria de 4 GB estimados para que, sensatamente, decida fazer a SORToperação antes da JOIN.

Você pode forçar a ORDER BYconsulta a adotar a ORDER BYestratégia de não associação usando as dicas abaixo.

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 

O plano mostra um operador de classificação com um custo estimado da subárvore de 12,000contagens estimadas de linhas quase e erradas e tamanho estimado dos dados.

Plano

Aliás, não achei que a substituição das UNIQUEIDENTIFIERcolunas por números inteiros alterasse as coisas no meu teste.

Martin Smith
fonte
2

Ligue o botão Mostrar plano de execução e você poderá ver o que está acontecendo. Aqui está o plano para a consulta "lenta": insira a descrição da imagem aqui

E aqui está a consulta "rápida": insira a descrição da imagem aqui

Olhe para isso - juntos, a primeira consulta é ~ 33x mais "cara" (proporção de 97: 3). O SQL está otimizando a primeira consulta para ordenar a BigTable por data e hora e, em seguida, executando um pequeno loop de "busca" em SmallTable e TinyTable, executando-as 1 milhão de vezes cada (você pode passar o mouse sobre o ícone "Clustered Index Seek" para obter mais estatísticas). Portanto, o tipo (27%) e 2 x 1 milhão de "pesquisas" em tabelas pequenas (23% e 46%) são a maior parte da consulta cara. Em comparação, a não ORDER BYconsulta realiza um total geral de três varreduras.

Basicamente, você encontrou um buraco na lógica do otimizador de SQL para o seu cenário específico. Mas, como declarado pelo TysHTTP, se você adicionar um índice (que diminui sua inserção / atualiza algumas), sua verificação fica louca rapidamente.

jklemmack
fonte
2

O que está acontecendo é que o SQL está decidindo executar o pedido antes da restrição.

Tente o seguinte:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

Isso fornece o desempenho aprimorado (nesse caso, em que a contagem de resultados retornada é muito pequena), sem que o desempenho seja atingido ao adicionar outro índice. Embora seja estranho quando o otimizador SQL decide executar a ordem antes da junção, é provável que, se você realmente tivesse retornado dados, a classificação depois das junções levaria mais tempo do que a classificação sem.

Por fim, tente executar o seguinte script e verifique se as estatísticas e os índices atualizados corrigem o problema que você está tendo:

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "
Seph
fonte