Por que adicionar um TOP 1 piora drasticamente o desempenho?

39

Eu tenho uma consulta bastante simples

SELECT TOP 1 dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Isso está me dando um desempenho horrível (como nunca se incomodou em esperar que terminasse). O plano de consulta fica assim:

insira a descrição da imagem aqui

No entanto, se eu remover TOP 1, recebo um plano parecido com este e é executado em 1-2 segundos:

insira a descrição da imagem aqui

Corrija PK e indexação abaixo.

O fato de o TOP 1plano de consulta alterado não me surpreender, estou um pouco surpreso que isso o torne muito pior.

Nota: Li os resultados deste post e entendi o conceito de Row Goaletc. O que me interessa é como posso alterar a consulta para que ela use o melhor plano. Atualmente, estou despejando os dados em uma tabela temporária e retirando a primeira linha dela. Eu estou querendo saber se existe um método melhor.

Editar Para as pessoas que leem isso após o fato, aqui estão algumas informações extras.

  • Document_Queue - PK / CI é D_ID e possui ~ 5k linhas.
  • Correspondence_Journal - PK / CI é FILE_NUMBER, CORRESPONDENCE_ID e possui ~ 1,4 mil linhas.

Quando comecei, não havia outros índices. Acabei com um em Correspondence_Journal (Document_Id, File_Number)

Kenneth Fisher
fonte
11
Você tem uma restrição de chave estrangeira que impõe o DOCUMENT_IDrelacionamento entre as duas tabelas (ou cada registro CORRESPONDENCE_JOURNALpossui um registro correspondente DOCUMENT_QUEUE)?
Daniel Hutmacher 28/01

Respostas:

28

Tente forçar uma junção de hash *

SELECT TOP 1 
       dc.DOCUMENT_ID,
       dc.COPIES,
       dc.REQUESTOR,
       dc.D_ID,
       cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
INNER HASH JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
       AND dc.QUEUE_DATE <= GETDATE()
       AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

O otimizador provavelmente pensou que um loop seria melhor com o top 1 e isso faz sentido, mas, na realidade, não funcionou aqui. Apenas um palpite aqui, mas talvez o custo estimado desse spool esteja desligado - ele usa TEMPDB - você pode ter um TEMPDB com desempenho ruim.


* Tenha cuidado com as dicas de junção , porque elas forçam a ordem de acesso à tabela de plano para corresponder à ordem escrita das tabelas na consulta (como se OPTION (FORCE ORDER)tivesse sido especificada). No link da documentação:

Extrato de BOL

Isso pode não produzir efeitos indesejáveis ​​no exemplo, mas, em geral, pode muito bem. FORCE ORDER(implícita ou explícita) é uma dica muito poderosa que vai além da imposição da ordem; impede que uma ampla variedade de técnicas de otimizador seja aplicada, incluindo agregações parciais e reordenação.

Uma dica de OPTION (HASH JOIN) consulta pode ser menos intrusiva em casos adequados, pois isso não implica FORCE ORDER. No entanto, aplica-se a todas as junções na consulta. Outras soluções estão disponíveis.

paparazzo
fonte
11
Parece que a resposta correta e a única diferença entre ela e o plano mais simples era uma classificação adicional na frente.
Kenneth Fisher
3
Não tenho certeza se gosto desta resposta. As dicas de junção são muito invasivas. Algumas alterações simples de indexação devem ser tentadas primeiro, por exemplo, índice na coluna da data.
usr
@usr É uma junção PK simples que é executada em menos de um segundo. Aposta bastante segura aqui.
paparazzo
4
Ao forçar uma junção de hash, você está forçando uma varredura da tabela grande. Existem melhores opções.
Rob Farley
30

Como você obtém o plano correto com o ORDER BY, talvez você possa simplesmente rolar seu próprio TOPoperador?

SELECT DOCUMENT_ID, COPIES, REQUESTOR, D_ID, FILE_NUMBER
FROM (
    SELECT dc.DOCUMENT_ID,
           dc.COPIES,
           dc.REQUESTOR,
           dc.D_ID,
           cj.FILE_NUMBER,
           ROW_NUMBER() OVER (ORDER BY cj.FILE_NUMBER) AS _rownum
    FROM DOCUMENT_QUEUE dc
    INNER JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
    WHERE dc.QUEUE_DATE <= GETDATE()
      AND dc.PRINT_LOCATION = 2
) AS sub
WHERE _rownum=1;

Na minha opinião, o plano de consulta ROW_NUMBER()acima deve ser o mesmo que se você tivesse um ORDER BY. O plano de consulta agora deve ter um segmento, projeto de sequência e, finalmente, um operador de filtro, o restante deve parecer exatamente como o seu bom plano.

Daniel Hutmacher
fonte
3
Na verdade, apesar de fornecer ao operador principal (e um monte de outras coisas (um projeto, segmento e classificação de sequência)), ele ainda executava subsegundos. Vou dar a resposta correta para o @frisbee desde que ele foi o primeiro e é mais simples. Ótima resposta embora.
Kenneth Fisher
10
@KennethFisher, a resposta do frisbee é mais simples, mas da maneira que uma marreta aciona uma unha de acabamento mais simplesmente do que um martelo de armação padrão. Ele também apresenta muitos riscos, principalmente se for deixado no local por um longo período. Eu não usaria dicas desse tipo, exceto nos testes ou talvez, talvez seja uma exceção.
Steve Mangiameli
@SteveMangiameli Neste caso em particular, existe apenas uma que se une, e várias preocupações desaparecem. Estou ciente dos riscos de usar uma dica de junção (ou dica de consulta), apenas acho que é justificada neste caso.
Kenneth Fisher
5
@KennethFisher Imo, o principal risco de dicas de consulta é que, à medida que seus dados aumentam ou mudam, o plano de consulta que você aplica pode se tornar pior do que o que o sistema teria encontrado sozinho. Você já viu como um pequeno erro no plano pode afetar seriamente o desempenho. Usar uma dica na produção está declarando: "Eu sei que esse plano será sempre, sempre o melhor, porque eu entendo perfeitamente o planejador e como meus dados se comportarão durante a vida útil dessa consulta na produção". Eu nunca estive tão confiante com uma consulta.
Jpmc26
29

Edit: +1 funciona nessa situação, porque FILE_NUMBERé uma versão de string preenchida com zero de um número inteiro. Uma solução melhor aqui para seqüências de caracteres é anexar ''(a sequência vazia), pois a adição de um valor pode afetar a ordem ou os números adicionarem algo que é uma constante, mas contém uma função não determinística, como sign(rand()+1). A idéia de 'quebrar o tipo' ainda é válida aqui, mas meu método não era ideal.

+1

Não, não quero dizer que estou de acordo com nada, quero dizer isso como uma solução. Se você alterar sua consulta para ORDER BY cj.FILE_NUMBER + 1, o TOP 1comportamento será diferente.

Veja que, com a pequena meta de linha em vigor para uma consulta ordenada, o sistema tentará consumir os dados em ordem, para evitar ter um operador de classificação. Ele também evitará criar uma tabela de hash, imaginando que provavelmente não precisa fazer muito trabalho para encontrar a primeira linha. No seu caso, isso está errado - pela espessura dessas setas, parece que é preciso consumir muitos dados para encontrar uma única correspondência.

A espessura dessas setas sugere que a DOCUMENT_QUEUEtabela (DQ) é muito menor que a CORRESPONDENCE_JOURNALtabela (CJ). E que o melhor plano seria realmente verificar as linhas DQ até que uma linha CJ seja encontrada. Na verdade, é isso que o Query Optimizer (QO) faria se não tivesse esse problema ORDER BYlá dentro, muito bem suportado por um índice de cobertura no CJ.

Portanto, se você descartou ORDER BYcompletamente, espero que você obtenha um plano que envolva um loop aninhado, iterando pelas linhas no DQ, procurando no CJ para garantir que a linha exista. E com TOP 1isso isso parava depois que uma única linha era puxada.

Mas se você realmente precisar da primeira linha em FILE_NUMBERordem, poderá enganar o sistema para ignorar o índice que parece (incorretamente) ser útil, fazendo ORDER BY CJ.FILE_NUMBER+1- o que sabemos que manterá a mesma ordem de antes, mas principalmente o QO não. O QO se concentrará em obter o conjunto completo, para que um operador Top N Sort possa ser satisfeito. Esse método deve produzir um plano que contenha um operador Compute Scalar para calcular o valor do pedido e um operador Top N Sort para obter a primeira linha. Mas, à direita, você deve ver um Nested Loop agradável, fazendo muitas pesquisas no CJ. E melhor desempenho do que executar uma grande tabela de linhas que não corresponde a nada no DQ.

O Hash Match não é necessariamente horrível, mas se o conjunto de linhas que você está retornando do DQ é muito menor que o CJ (como eu esperava que fosse), o Hash Match analisará muito mais o CJ do que precisa.

Nota: usei +1 em vez de +0 porque é provável que o otimizador de consultas reconheça que +0 não altera nada. Obviamente, o mesmo pode ser aplicado ao +1, se não agora, em algum momento no futuro.

Rob Farley
fonte
7

Li os resultados desta postagem e entendi o conceito de uma meta de linha etc. O que me interessa é como posso alterar a consulta para que ela use o melhor plano

A adição OPTION (QUERYTRACEON 4138)desativa o efeito das metas de linha apenas para essa consulta, sem ser excessivamente prescritiva sobre o plano final e provavelmente será a maneira mais simples / direta.

Se a adição dessa dica der um erro de permissão (necessário DBCC TRACEON), você poderá aplicá-la usando um guia de plano:

Usando QUERYTRACEONnos guias de plano de spaghettidba

... ou apenas use um procedimento armazenado:

Quais permissões são QUERYTRACEONnecessárias? por Kendra Little

Martin Smith
fonte
3

As versões mais recentes do SQL Server oferecem opções diferentes (e sem dúvida melhores) para lidar com consultas que obtêm desempenho abaixo do ideal quando o otimizador é capaz de aplicar otimizações de meta de linha. O SQL Server 2016 SP1 apresentou o DISABLE_OPTIMIZER_ROWGOAL USE HINTque tem o mesmo efeito que o sinalizador de rastreamento 4138. Se você não estiver nessa versão, também poderá usar a OPTIMIZE FORdica de consulta para obter um plano de consulta projetado para retornar todas as linhas em vez de apenas 1. A consulta abaixo retornará os mesmos resultados que o da pergunta, mas não será criado com o objetivo de obter apenas 1 linha.

DECLARE @top INT = 1;

SELECT TOP (@top) dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER
OPTION (OPTIMIZE FOR (@top = 987654321));
Joe Obbish
fonte
2

Como você está fazendo um TOP(1), eu recomendo fazer o ORDER BYdeterminístico para começar. No mínimo, isso garantirá que os resultados sejam funcionalmente previsíveis (sempre úteis para testes de regressão). Parece que você precisa adicionar DC.D_IDe CJ.CORRESPONDENCE_IDpara isso.

Ao examinar os planos de consulta, às vezes acho instrutivo simplificar a consulta: Possivelmente selecione todas as linhas dc relevantes em uma tabela temporária com antecedência, para eliminar problemas com a estimativa de cardinalidade em QUEUE_DATEe PRINT_LOCATION. Isso deve ser rápido, considerando o número de linhas baixo. Você pode adicionar índices a essa tabela temporária, se necessário, sem alterar a tabela permanente.

Simon Birch
fonte