Eu tenho um procedimento armazenado que preenche a tabela temporária #employee_benefits com uma lista de IDs. Essa tabela acaba tendo aproximadamente 10.000 linhas. A consulta abaixo seleciona de uma tabela chamada EmployeeBenefitData que tem cerca de 4 milhões de linhas.
SELECT ebd.EmployeeBenefitDataId, ebd.EmployeeBenefitId, ebd.[DataDefinitionId]
FROM #employee_benefits eb
INNER JOIN EmployeeBenefitData ebd ON eb.EmployeeBenefitId = ebd.EmployeeBenefitId
O gargalo era a varredura de índice na tabela EmployeeBenefitData. Ele fez a varredura de índice primeiro e depois a juntou à tabela temporária. A tabela temporária estava agindo como um filtro, o que significa que a varredura de todos os dados antes da junção era muito ineficiente. Adicionei o código a seguir para alterar a varredura para uma busca e reduzir drasticamente a quantidade de leituras necessárias.
DECLARE @MinEmpBenId INT, @MaxEmpBenId INT
SELECT @MinEmpBenId = MIN(EmployeeBenefitId), @MaxEmpBenId = MAX(EmployeeBenefitId)
FROM #employee_benefits
SELECT ebd.EmployeeBenefitDataId, ebd.EmployeeBenefitId, ebd.[DataDefinitionId],
dd.TypeId, dd.DataDefinitionId, dd.Name, ebd.[Value], ebd.[Date], ebd.[Text]
FROM #employee_benefits eb
INNER JOIN EmployeeBenefitData ebd ON eb.EmployeeBenefitId = ebd.EmployeeBenefitId
INNER JOIN DataDefinition dd ON ebd.DataDefinitionId = dd.DataDefinitionId
WHERE ebd.EmployeeBenefitId >= @MinEmpBenId AND ebd.EmployeeBenefitId <= @MaxEmpBenId
Faz uma enorme diferença nas estatísticas do cliente
Tempo total de execução 74, 1794
Tempo de espera nas respostas do servidor 11, 11
Minha pergunta é: isso é uma boa prática? E por que o otimizador não faz isso?
ATUALIZAÇÃO Eu deveria ter mencionado que a tabela temporária tem um índice clusterizado em EmployeeBenefitID
Nesta circunstância, eu diria que sim. Eu provavelmente também adicionaria um
OPTION (RECOMPILE)
para deixá-lo "cheirar" os valores das variáveis. O plano ideal provavelmente variará dependendo da proporção de linhas na tabela maior que correspondem a esse intervalo.Ele fornece um caminho extra potencialmente útil para o otimizador e não é algo que o otimizador de consulta faça sozinho, até onde eu sei. A coisa mais próxima disso é que, com uma junção de mesclagem, ela interromperá o processamento de uma entrada quando uma delas terminar. Isso significa que potencialmente evita uma verificação completa.
A única desvantagem que vem à mente seria se o cálculo dos valores mínimo/máximo do intervalo em si pode ser caro (mas isso deve ser muito barato se a tabela que você está usando como filtro estiver indexada nessa coluna).
Eu criei duas tabelas de teste
E carregou EmployeeBenefitData com números inteiros de 1 a 4.000.000 (6.456 páginas)
E FilteredEmployee com números inteiros de 2.000.000 E 2.010.000 (19 páginas)
E então executou 6 consultas do seguinte formulário
As 6 permutações foram feitas invertendo a ordem das duas tabelas e tentando todos os três tipos de junção
LOOP
,MERGE
,HASH
.Os resultados foram os seguintes
As figuras acima ilustram o ponto sobre a junção de mesclagem, pois ela "só" varre pouco mais da metade da tabela maior. Ele ainda leu todas as linhas de 1 a 1.999.999 primeiro e as descartou.
Repetindo o experimento com a
WHERE EBD.EmployeeID BETWEEN 2000000 AND 2010000
deu o seguinte.A única consulta que não se beneficiou do predicado de intervalo adicional é aquela em que a tabela maior estava dentro de uma junção de loops aninhados.
Obviamente, isso não é surpreendente, pois esse plano (plano 1 abaixo) é conduzido por buscas repetidas de índice usando os valores de
FilteredEmployee
.O plano 1 também foi o escolhido "naturalmente" pelo otimizador sem o predicado de intervalo. Com o predicado de intervalo em vigor, ele escolheu um plano de junção de mesclagem diferente, buscando no intervalo de índice relevante sem digitalizar linhas desnecessárias e custou significativamente mais barato (plano 2)
Já vi o planejador de consulta fazer coisas semelhantes com tabelas temporárias sem índice, mesmo às vezes quando o número de linhas na tabela temporária é pequeno.
Tente adicionar um índice que cubra as colunas que você está juntando e filtrando, para ver se o planejador usa as estatísticas delas para perceber que pode atingir a meta de maneira mais eficiente.
Além disso: se você tiver índices apropriados em todas as tabelas que estão sendo unidas, poderá adicionar uma dica de índice (adicionando
WITH(INDEX(<index_name>))
após a cláusula from) para fazer o planejador de consulta seguir uma determinada direção, embora isso torne suas visualizações/procs dependentes de nomes de índice e força o planejador de consultas a seguir uma rota específica, quando poderia ter escolhido uma rota melhor posteriormente, à medida que seus dados crescem/alteram.