Por que selecionar todas as colunas resultantes desta consulta é mais rápida que selecionar a coluna que mais me interessa?

13

Eu tenho uma consulta em que o uso select *não apenas faz muito menos leituras, mas também usa significativamente menos tempo de CPU do que o uso select c.Foo.

Esta é a consulta:

select top 1000 c.ID
from ATable a
    join BTable b on b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
    join CTable c on c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
where (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff)
    and b.IsVoided = 0
    and c.ComplianceStatus in (3, 5)
    and c.ShipmentStatus in (1, 5, 6)
order by a.LastAnalyzedDate

Isso terminou com 2.473.658 leituras lógicas, principalmente na Tabela B. Usou 26.562 CPU e teve uma duração de 7.965.

Este é o plano de consulta gerado:

Planejar a partir de Selecionando o valor de uma única coluna No PasteThePlan: https://www.brentozar.com/pastetheplan/?id=BJAp2mQIQ

Quando mudo c.IDpara *, a consulta terminou com 107.049 leituras lógicas, distribuídas uniformemente entre as três tabelas. Usou 4.266 CPU e teve uma duração de 1.147.

Este é o plano de consulta gerado:

Planejar de Selecionando todos os valores No PasteThePlan: https://www.brentozar.com/pastetheplan/?id=SyZYn7QUQ

Tentei usar as dicas de consulta sugeridas por Joe Obbish, com os seguintes resultados:
select c.IDsem dica: https://www.brentozar.com/pastetheplan/?id=SJfBdOELm
select c.ID com dica: https://www.brentozar.com/pastetheplan/ ? id = B1W ___ N87
select * sem dica: https://www.brentozar.com/pastetheplan/?id=HJ6qddEIm
select * com dica: https://www.brentozar.com/pastetheplan/?id=rJhhudNIQ

O uso da OPTION(LOOP JOIN)dica com select c.IDreduziu drasticamente o número de leituras em comparação com a versão sem a dica, mas ainda está fazendo cerca de 4x o número de leituras da select *consulta sem nenhuma dica. A adição OPTION(RECOMPILE, HASH JOIN)à select *consulta fez com que o desempenho fosse muito pior do que qualquer outra coisa que tentei.

Após atualizar as estatísticas nas tabelas e nos índices usando WITH FULLSCAN, a select c.IDconsulta está executando muito mais rapidamente:
select c.IDantes da atualização: https://www.brentozar.com/pastetheplan/?id=SkiYoOEUm
select * antes da atualização: https://www.brentozar.com/ pastetheplan /? id = ryrvodEUX
select c.ID após a atualização: https://www.brentozar.com/pastetheplan/?id=B1MRoO487
select * após a atualização: https://www.brentozar.com/pastetheplan/?id=Hk7si_V8m

select *ainda supera select c.IDem termos de duração total e total de leituras ( select *tem cerca de metade das leituras), mas usa mais CPU. No geral, eles estão muito mais próximos do que antes da atualização, no entanto, os planos ainda diferem.

O mesmo comportamento é observado em 2016, em execução no modo de compatibilidade de 2014 e em 2014. O que poderia explicar a disparidade entre os dois planos? Será que os índices "corretos" não foram criados? As estatísticas poderiam estar um pouco desatualizadas?

Tentei mover os predicados para a ONparte da junção, de várias maneiras, mas o plano de consulta é o mesmo a cada vez.

Após a recriação do índice

Eu reconstruí todos os índices nas três tabelas envolvidas na consulta. c.IDainda está fazendo o máximo de leituras (mais do dobro *), mas o uso da CPU é cerca da metade da *versão. A c.IDversão também derramado tempdb na classificação de ATable:
c.ID: https://www.brentozar.com/pastetheplan/?id=HyHIeDO87
* : https://www.brentozar.com/pastetheplan/?id=rJ4deDOIQ

Também tentei forçá-lo a operar sem paralelismo, e isso me deu a consulta com melhor desempenho: https://www.brentozar.com/pastetheplan/?id=SJn9-vuLX

Percebo a contagem de execução dos operadores APÓS o grande índice procurar que está executando a ordenação executada apenas 1.000 vezes na versão single-threaded, mas fez significativamente mais na versão paralela, entre 2.622 e 4.315 execuções de vários operadores.

L. Miller
fonte

Respostas:

4

É verdade que selecionar mais colunas implica que o SQL Server precise trabalhar mais para obter os resultados solicitados da consulta. Se o otimizador de consulta conseguiu criar o plano de consulta perfeito para as duas consultas, seria razoável esperar que oSELECT *consulta para ser executada por mais tempo que a consulta que seleciona todas as colunas de todas as tabelas. Você observou o oposto do seu par de consultas. Você precisa ter cuidado ao comparar custos, mas a consulta lenta tem um custo total estimado de 1090.08 unidades otimizadoras e a consulta rápida tem um custo total estimado de 6823.11 unidades otimizadoras. Nesse caso, pode-se dizer que o otimizador faz um trabalho ruim com a estimativa dos custos totais de consulta. Ele escolheu um plano diferente para sua consulta SELECT * e esperava que o plano fosse mais caro, mas esse não foi o caso aqui. Esse tipo de incompatibilidade pode ocorrer por vários motivos e uma das causas mais comuns são os problemas de estimativa de cardinalidade. Os custos do operador são amplamente determinados por estimativas de cardinalidade. Se uma estimativa de cardinalidade em um ponto-chave de um plano for imprecisa, o custo total do plano poderá não refletir a realidade. Essa é uma simplificação grosseira, mas espero que seja útil para entender o que está acontecendo aqui.

Vamos começar discutindo por que uma SELECT *consulta pode ser mais cara do que selecionar uma única coluna. A SELECT *consulta pode transformar alguns índices de cobertura em índices não cobertos, o que pode significar que o otimizador precisa fazer um trabalho adicional para obter todas as colunas necessárias ou pode precisar ler de um índice maior.SELECT *também pode resultar em conjuntos de resultados intermediários maiores que precisam ser processados ​​durante a execução da consulta. Você pode ver isso em ação observando os tamanhos de linha estimados em ambas as consultas. Na consulta rápida, os tamanhos das linhas variam de 664 bytes a 3019 bytes. Na consulta lenta, os tamanhos das linhas variam de 19 a 36 bytes. Operadores de bloqueio, como classificações ou compilações de hash, terão custos mais altos para dados com um tamanho de linha maior, porque o SQL Server sabe que é mais caro classificar quantidades maiores de dados ou transformá-las em uma tabela de hash.

Analisando a consulta rápida, o otimizador estima que precisa realizar 2,4 milhões de buscas no índice Database1.Schema1.Object5.Index3. É daí que vem a maior parte do custo do plano. No entanto, o plano real revela que apenas 1332 buscas de índice foram feitas nesse operador. Se você comparar as linhas reais e estimadas para as partes externas dessas junções de loop, verá grandes diferenças. O otimizador acha que serão necessárias muitas outras pesquisas de índice para encontrar as primeiras 1000 linhas necessárias para os resultados da consulta. É por isso que a consulta tem um plano de custo relativamente alto, mas termina com tanta rapidez: o operador que foi considerado o mais caro realizou menos de 0,1% do trabalho esperado.

Olhando para a consulta lenta, você obtém um plano com junções de hash (acredito que a junção de loop existe apenas para lidar com a variável local). As estimativas de cardinalidade definitivamente não são perfeitas, mas o único problema real de estimativa está no final da classificação. Suspeito que a maior parte do tempo seja gasta nas varreduras das tabelas com centenas de milhões de linhas.

Você pode achar útil adicionar dicas de consulta às duas versões da consulta para forçar o plano de consulta associado à outra versão. As dicas de consulta podem ser uma boa ferramenta para descobrir por que o otimizador fez algumas de suas escolhas. Se você adicionar OPTION (RECOMPILE, HASH JOIN)à SELECT *consulta, espero que você veja um plano de consulta semelhante à consulta de junção de hash. Também espero que os custos da consulta sejam muito maiores para o plano de junção de hash, porque os tamanhos das linhas são muito maiores. Portanto, pode ser por isso que a consulta de junção de hash não foi escolhida para a SELECT *consulta. Se você adicionar OPTION (LOOP JOIN)à consulta que seleciona apenas uma coluna, espero que você veja um plano de consulta semelhante ao da colunaSELECT *inquerir. Nesse caso, reduzir o tamanho da linha não deve afetar muito o custo geral da consulta. Você pode pular as principais pesquisas, mas essa é uma pequena porcentagem do custo estimado.

Em resumo, espero que os tamanhos de linhas maiores necessários para satisfazer a SELECT *consulta levem o otimizador para um plano de junção de loop em vez de um plano de junção de hash. O plano de junção de loop é mais caro do que deveria devido a problemas de estimativa de cardinalidade. Reduzir o tamanho da linha selecionando apenas uma coluna reduz bastante o custo de um plano de junção de hash, mas provavelmente não terá muito efeito sobre o custo de um plano de junção de loop, portanto, você acaba com o plano de junção de hash menos eficiente. É difícil dizer mais do que isso para um plano anônimo.

Joe Obbish
fonte
Muito obrigado pela sua resposta abrangente e informativa. Tentei adicionar as dicas que você sugeriu. Tornou a select c.IDconsulta muito mais rápida, mas ainda está fazendo algum trabalho extra que a select *consulta, sem dicas, faz.
L. Miller
2

Estatísticas obsoletas certamente podem fazer com que o otimizador escolha um método ruim de localizar os dados. Você já tentou fazer um UPDATE STATISTICS ... WITH FULLSCANou um total REBUILDno índice? Tente isso e veja se ajuda.

ATUALIZAR

De acordo com uma atualização do OP:

Após atualizar as estatísticas nas tabelas e seus índices usando WITH FULLSCAN, a select c.IDconsulta está sendo executada muito mais rapidamente

Portanto, agora, se a única ação tomada foi UPDATE STATISTICS, tente executar um índice REBUILD(não REORGANIZE), como já vi, que ajuda na contagem estimada de linhas, onde ambos UPDATE STATISTICSe o índice REORGANIZEnão.

Solomon Rutzky
fonte
Consegui obter todos os índices das três tabelas envolvidas para reconstruir no fim de semana e atualizei minha postagem para refletir esses resultados.
L. Miller
-1
  1. Você pode incluir os scripts de índice?
  2. Você eliminou possíveis problemas com o "sniffing de parâmetros"? https://www.mssqltips.com/sqlservertip/3257/different-approaches-to-correct-sql-server-parameter-sniffing/
  3. Eu achei esta técnica útil em alguns casos:
    a) reescreva cada tabela como uma subconsulta, seguindo estas regras:
    b) SELECT - coloque primeiro as colunas de junção
    c) PREDICATES - mude para as respectivas subconsultas
    d) ORDER BY - mude para a subconsultas respectivas, classifique em JOIN COLUMNS FIRST
    e) Adicione uma consulta de wrapper para sua classificação final e SELECT.

A idéia é pré-classificar as colunas de junção dentro de cada subseleção, colocando as colunas de junção primeiro em cada lista de seleção.

Aqui está o que eu quero dizer ....

SELECT ... wrapper query
FROM
(
    SELECT ...
    FROM
        (SELECT ClientID, ShipKey, NextAnalysisDate
         FROM ATABLE
         WHERE (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff) -- Predicates
         ORDER BY OrderKey, ClientID, LastAnalyzedDate  ---- Pre-sort the join columns
        ) as a
        JOIN 
        (SELECT OrderKey, ClientID, OrderID, IsVoided
         FROM BTABLE
         WHERE IsVoided = 0             ---- Include all predicates
         ORDER BY OrderKey, OrderID, IsVoided       ---- Pre-sort the join columns
        ) as b ON b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
        JOIN
        (SELECT OrderID, ShipKey, ComplianceStatus, ShipmentStatus, ID
         FROM CTABLE
         WHERE ComplianceStatus in (3, 5)       ---- Include all predicates
             AND ShipmentStatus in (1, 5, 6)        ---- Include all predicates
         ORDER BY OrderID, ShipKey          ---- Pre-sort the join columns
        ) as c ON c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
) as d
ORDER BY d.LastAnalyzedDate
Victor Di Leo
fonte
1
1. Tentarei adicionar scripts DDL de índice à postagem original, que pode demorar um pouco para "limpá-los". 2. Testei essa possibilidade limpando o cache do plano antes da execução e substituindo o parâmetro bind por um valor real. 3. Tentei isso, mas ORDER BYé inválido em uma subconsulta sem TOP, FORXML, etc. Tentei sem as ORDER BYcláusulas, mas era o mesmo plano.
1717 Miller Miller