Otimize a seleção na subconsulta com COALESCE (…)

8

Eu tenho uma visão ampla que uso de dentro de um aplicativo. Acho que reduzi meu problema de desempenho, mas não tenho certeza de como corrigi-lo. Uma versão simplificada da exibição é assim:

SELECT ISNULL(SEId + '-' + PEId, '0-0') AS Id,
   *,
   DATEADD(minute, Duration, EventTime) AS EventEndTime
FROM (
    SELECT se.SEId, pe.PEId,
        COALESCE(pe.StaffName, se.StaffName) AS StaffName, -- << Problem!
        COALESCE(pe.EventTime, se.EventTime) AS EventTime,
        COALESCE(pe.EventType, se.EventType) AS EventType,
        COALESCE(pe.Duration, se.Duration) AS Duration,
        COALESCE(pe.Data, se.Data) AS Data,
        COALESCE(pe.Field, se.Field) AS Field,
        pe.ThisThing, se.OtherThing
    FROM PE pe FULL OUTER JOIN SE se 
      ON pe.StaffName = se.StaffName
     AND pe.Duration = se.Duration
     AND pe.EventTime = se.EventTime
    WHERE NOT(pe.ThisThing = 1 AND se.OtherThing = 0)
) Z

Isso provavelmente não justifica toda a razão da estrutura da consulta, mas talvez lhe dê uma idéia - essa visão une duas tabelas mal projetadas que eu não tenho controle e tenta sintetizar algumas informações dela.

Portanto, como essa é uma visão usada do aplicativo, ao tentar otimizar, envolvo-a em outro SELECT, assim:

SELECT * FROM (
    -- … above code …
) Q
WHERE StaffName = 'SMITH, JOHN Q'

porque o aplicativo está procurando membros específicos da equipe no resultado.

O problema parece ser a COALESCE(pe.StaffName, se.StaffName) AS StaffNameseção e estou selecionando a partir da exibição StaffName. Se eu mudar para pe.StaffName AS StaffNameou se.StaffName AS StaffName, os problemas de desempenho desaparecerão (mas consulte a atualização 2 abaixo) . Mas isso não acontece porque um lado ou o outro FULL OUTER JOINpode estar ausente, portanto, um ou outro campo pode ser NULL.

Posso refatorar isso substituindo o COALESCE(…)por outra coisa, que será reescrita na subconsulta?

Outras notas:

  • Eu já adicionei alguns índices para corrigir problemas de desempenho com o restante da consulta - sem COALESCEque seja muito rápido.
  • Para minha surpresa, observar o plano de execução não gera nenhum sinalizador, mesmo quando a subconsulta e a WHEREinstrução de empacotamento estão incluídas. Meu custo total de subconsulta no analisador é 0.0065736. Hmph. Demora quatro segundos para executar.
  • Alterar o aplicativo para consultar de maneira diferente (por exemplo, retornar pe.StaffName AS PEStaffName, se.StaffName AS SEStaffNamee executar WHERE PEStaffName = 'X' OR SEStaffName = 'X') pode funcionar, mas como último recurso - espero realmente otimizar a exibição sem precisar recorrer ao toque no aplicativo.
  • Um procedimento armazenado provavelmente faria mais sentido para isso, mas o aplicativo é construído com o Entity Framework, e eu não conseguia descobrir como fazê-lo funcionar bem com um SP que retorna um tipo de tabela (outro tópico inteiramente).

Índices

Os índices que adicionei até agora são mais ou menos assim:

CREATE NONCLUSTERED INDEX [IX_PE_EventTime]
ON [dbo].[PE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[ThisThing])

CREATE NONCLUSTERED INDEX [IX_SE_EventTime]
ON [dbo].[SE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[OtherThing])

Atualizar

Hmm ... eu tentei simular a mudança atingida acima, e não ajudou. Ou seja, antes ) Z, acrescentei AND (pe.StaffName = 'SMITH, JOHN Q' OR se.StaffName = 'SMITH, JOHN Q'), mas o desempenho é o mesmo. Agora realmente não sei por onde começar.

Atualização 2

O comentário do @ypercube sobre a necessidade da junção completa me fez perceber que minha consulta sintetizada deixou de fora um componente provavelmente importante. Embora, sim, eu precise da junção completa, o teste que fiz acima, eliminando COALESCEe testando apenas um lado da junção para um valor não nulo, tornaria o outro lado da junção completa irrelevante , e o otimizador provavelmente estava usando este fato para acelerar a consulta. Além disso, atualizei o exemplo para mostrar que, StaffNamena verdade, é uma das chaves de junção - que provavelmente tem uma influência significativa sobre a questão. Agora também estou inclinado a sugerir que quebrar isso em uma união de três vias, em vez de uma união completa, pode ser a resposta e simplificará a abundância de COALESCEs que estou fazendo de qualquer maneira. Tentando agora.

S'pht'Kr
fonte
Quais índices você adicionou? Você está incluindo o StaffName no índice?
Mark Sinkinson
@ MarkSinkinson Eu tenho um índice não clusterizado em cada tabela KeyField, ambos indexam INCLUDEo StaffNamecampo e vários outros campos. Eu posso postar as definições de índice na pergunta. Estou trabalhando nisso em um servidor de teste para adicionar quaisquer índices que você julgue úteis!
S'pht'Kr
11
Você tem a WHERE pe.ThisThing = 1 AND se.OtherThing = 0condição que cancela a FULL OUTERjunção e torna a consulta equivalente a uma junção interna. Tem certeza de que precisa de uma associação COMPLETA?
ypercubeᵀᴹ
@ypercube Sinto muito, isso foi um mau código de ar da minha parte, o ponto é mais que tenho condições em ambas as tabelas, mas sim, eu respondo por nulos de ambos os lados na consulta real. Estou mesclando as duas tabelas e procurando correspondências, mas preciso dos dados disponíveis em ambas as tabelas quando não houver um registro correspondente à esquerda ou à direita - portanto, sim, preciso da associação completa.
S'pht'Kr
11
Um pensamento: é uma possibilidade remota, mas você pode tentar quebrar a consulta interna em três partes ( INNER JOIN, LEFT JOINcom WHERE IS NULLcheque, RIGHT JOIN com IS NULL) e, em seguida, UNION ALLas três partes. Dessa forma, não haverá necessidade de uso COALESCE()e poderá (apenas poderá) ajudar o otimizador a descobrir a reescrita.
ypercubeᵀᴹ

Respostas:

4

Isso foi muito demorado, mas como o OP diz que funcionou, estou adicionando-o como resposta (fique à vontade para corrigi-lo se encontrar algo errado).

Tente dividir a consulta interna em três partes ( INNER JOIN, LEFT JOINcom WHERE IS NULLcheque, RIGHT JOINcom IS NULLcheque) e depois UNION ALLnas três partes. Isso tem as seguintes vantagens:

  • O otimizador possui menos opções de transformação disponíveis para FULLjunções do que para (as mais comuns) INNERe LEFTjunções.

  • A Ztabela derivada pode ser removida (você pode fazer isso de qualquer maneira) da definição da visualização.

  • O NOT(pe.ThisThing = 1 AND se.OtherThing = 0)serão necessários apenas na INNERparte aderir.

  • Pequenas melhorias, o uso COALESCE()será mínimo, se houver (presumi que se.SEIde pe.PEIdnão sejam anuláveis. Se mais colunas não forem anuláveis, você poderá remover mais COALESCE()chamadas.)
    Mais importante, o otimizador pode empurrar para baixo quaisquer condições em suas consultas que envolvem essas colunas (agora que COALESCE()não estão bloqueando o envio).

  • Todas as opções acima fornecerão ao otimizador mais opções para transformar / reescrever qualquer consulta que use a visualização, para que ele encontre um plano de execução que possa ser usado nos índices das tabelas subjacentes.

Ao todo, a visualização pode ser escrita como:

SELECT 
    se.SEId + '-' + pe.PEId AS Id,
    se.SEId, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    COALESCE(pe.EventType, se.EventType) AS EventType,
    pe.Duration,
    COALESCE(pe.Data, se.Data) AS Data,
    COALESCE(pe.Field, se.Field) AS Field,
    pe.ThisThing, se.OtherThing,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe INNER JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1 AND se.OtherThing = 0) 

UNION ALL

SELECT 
    '0-0',
    NULL, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    pe.EventType,
    pe.Duration,
    pe.Data,
    pe.Field,
    pe.ThisThing, NULL,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe LEFT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1)
  AND se.StaffName IS NULL

UNION ALL

SELECT 
    '0-0',
    se.SEId, NULL,
    se.StaffName, 
    se.EventTime,
    se.EventType,
    se.Duration,
    se.Data,
    se.Field,
    NULL, se.OtherThing, 
    DATEADD(minute, se.Duration, se.EventTime) AS EventEndTime
FROM PE pe RIGHT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (se.OtherThing = 0)
  AND pe.StaffName IS NULL ;
ypercubeᵀᴹ
fonte
0

Minha intuição seria que isso não deveria ser um problema, já que, no momento em COALESCE(pe.StaffName, se.StaffName) AS StaffNameque algo faz com que todas as linhas das duas fontes já tenham sido extraídas e correspondidas, a chamada de função é uma simples comparação na memória com a nula e -escolher. Obviamente, esse não é o caso, então talvez algo em uma das fontes (se forem visualizações ou tabelas derivadas em linha) ou nas tabelas base (por exemplo, falta de índices) está fazendo o planejador de consultas achar que precisa examinar essas colunas separadamente.

Sem mais detalhes da consulta exata que você está executando, das estruturas de suporte e dos planos de consulta produzidos, tudo o que sugerimos é conjectura.

Para tentar forçar a comparação a ser feita depois de tudo, tente selecionar os dois valores na tabela abaixo ( pe.StaffName AS pe.StaffName, se.StaffName AS seStaffName) e faça a seleção na consulta externa ( COALESCE(peStaffName, seStaffName) AS StaffName) ou até mesmo enviar os dados da consulta interna para uma tabela temporária faz a consulta externa selecionando-a (mas isso exigiria um procedimento armazenado e, dependendo do número de linhas, esse dump para tempdb pode ser caro e, portanto, problemático por si só).

David Spillett
fonte
Obrigado David, eu errei do lado da paranóia quanto ao quanto devo revelar sobre isso, até a estrutura (pe => PatientEvent, então ...), mas sei que isso dificulta. Eu acho que é de fato fazer a junção com base em índices e, em seguida, fazer uma "comparação simples na memória" para filtrar ... mas a tabela derivada não filtrada Zatualmente volta com ~ 1.5m linhas. O que eu quero fazer é reescrever esse predicado na consulta para Zque ele use os índices ... mas agora também estou confuso porque quando eu coloco o predicado manualmente lá, ele ainda não usa um índice ... então agora Não tenho certeza.
S'pht'Kr