Eu tenho 3 tabelas: Room
, Conference
, e Participant
. Room
tem muitos Conference
s, e Conference
tem muitos Participant
s. Eu preciso que minha consulta exiba os campos de Room
, bem como o número de associados Conferences
que ele possui e a soma do número de Participant
s associados que cada um Conference
possui. Aqui está uma versão reduzida da SELECT
consulta que escrevi para obter essas informações; primeiro, selecionei o ID da sala:
SELECT TOP(1000)
rm.[Id]
FROM
[Room] rm
LEFT JOIN (
SELECT
conf.[Id] AS [ConferenceId],
MIN(conf.[Name]) AS [ConferenceName],
MIN(conf.[RoomId]) AS [RoomId],
COUNT(part.[Id]) AS CalcConferenceParticipantCount
FROM
[Conference] conf
LEFT JOIN
[Participant] part on part.[ConferenceId] = conf.[Id]
GROUP BY
conf.[Id]
) confData ON confData.[RoomId] = rm.[Id]
GROUP BY
rm.[Id]
Isso foi muito rápido, pois o SQL Server conseguiu extrair os dados Room
e praticamente ignorar a subconsulta (consulte Avaliação 1 - Avaliação 4 na imagem abaixo). Em seguida, adicionei no ConferenceName
campo da subconsulta, bem como uma contagem do número de conferências por sala:
SELECT TOP(1000)
rm.[Id],
COUNT(confData.[ConferenceId]) AS CalcRoomConferenceCount,
MIN(confData.[ConferenceName])
FROM
[Room] rm
LEFT JOIN (
SELECT
conf.[Id] AS [ConferenceId],
MIN(conf.[Name]) AS [ConferenceName],
MIN(conf.[RoomId]) AS [RoomId],
COUNT(part.[Id]) AS CalcConferenceParticipantCount
FROM
[Conference] conf
LEFT JOIN
[Participant] part on part.[ConferenceId] = conf.[Id]
GROUP BY
conf.[Id]
) confData ON confData.[RoomId] = rm.[Id]
GROUP BY
rm.[Id]
Isso desacelerou um pouco a consulta, por um fator de cerca de 100 (veja Teste 5 - Teste 7 na imagem abaixo). Em seguida, adicionei a contagem de participantes da subconsulta, o que significa que havia 2 níveis de funções agregadas sendo usadas:
SELECT TOP(1000)
rm.[Id],
COUNT(confData.[ConferenceId]) AS CalcRoomConferenceCount,
MIN(confData.[ConferenceName]),
SUM(confData.[CalcConferenceParticipantCount]) AS CalcRoomParticipantCount
FROM
[Room] rm
LEFT JOIN (
SELECT
conf.[Id] AS [ConferenceId],
MIN(conf.[Name]) AS [ConferenceName],
MIN(conf.[RoomId]) AS [RoomId],
COUNT(part.[Id]) AS CalcConferenceParticipantCount
FROM
[Conference] conf
LEFT JOIN
[Participant] part on part.[ConferenceId] = conf.[Id]
GROUP BY
conf.[Id]
) confData ON confData.[RoomId] = rm.[Id]
GROUP BY
rm.[Id]
Isso desacelerou ainda mais a consulta por um fator de cerca de 4 (veja Ensaio 8 - Ensaio 10 na imagem abaixo). Aqui estão as estatísticas do cliente com dados sobre os 10 testes:
Aqui está o plano de consulta da consulta lenta: https://www.brentozar.com/pastetheplan/?id=SJpyeec5Q
Existe uma maneira de tornar esse tipo de consulta - onde calculo uma agregação da agregação de uma subconsulta - mais eficiente?
Eu zombei de dados observando as contagens de linhas em suas tabelas, dando a elas uma distribuição de dados uniforme e fazendo suposições sobre o esquema:
A suposição mais importante que fiz sobre o esquema é que a
Id
coluna é a chave primária da[Conference]
tabela. Isso parecia razoável, dado o plano de consulta e os nomes de índice envolvidos.Na minha máquina, recebo o mesmo plano de consulta que você, mas minha consulta inicial leva apenas 163 ms de CPU. Presumo que as diferenças se resumem a diferenças de hardware, distribuição de dados e ao fato de não estar retornando dados ao cliente.
A primeira coisa que me chamou atenção é o desnecessário
GROUP BY
em suaconfData
tabela derivada.Id
é a chave primária da tabela para que você não precise de todos os agregados. Com os índices certos (que você já tem para este caso em particular), as subconsultas não são necessariamente uma coisa ruim. Reescrevendo o que você precisa remover oGROUP BY
:Isso resulta na agregação de fluxo sendo empurrada ainda mais para o plano:
O plano carregado leva 113 ms de CPU. Os mesmos operadores estão presentes, mas alguns deles processam menos linhas, o que economiza tempo. Você pode tornar essa consulta mais eficiente definindo um índice de cobertura no
[Conference]
comId
como a chave de índice. Isso pode parecer uma coisa estranha de se fazer, mas sua verificação de índice clusterizado leva 10% do tempo geral de consulta e provavelmente inclui colunas que você não precisa.Se você deseja tornar a consulta mais rápida, também pode considerar uma exibição indexada. Por que realizar a agregação sempre que você pode definir uma exibição indexada simples para fazer isso por você?
Isso resultará em um pouco mais de espaço e um pouco de sobrecarga ao fazer DML na mesa. No geral, eu diria que é um bom caso de uso para uma exibição indexada. Reescrevendo a consulta novamente:
O SQL Server concorda com minha avaliação de que é uma boa ideia e o tempo de CPU cai para 78 ms .
Na minha máquina consegui deixar a consulta ainda mais rápida, mas isso está começando a entrar em otimizações que são um pouco arriscadas porque podem exigir uma
LOOP JOIN
dica. Essa dica pode não ser uma boa ideia, pois sua consulta ou os dados na tabela mudam. Também pode não ser um bom ajuste para o seu hardware. A ideia por trás dessa abordagem é criar um índice adequado[Conference]
e aproveitar ao máximo oTOP
com um plano que só faz loops aninhados. Aqui está o índice que eu adicionei:A execução da mesma consulta de antes com uma
LOOP JOIN
dica me deu o seguinte plano:Essa consulta levou apenas 58 ms de tempo de CPU. Vale a pena mencionar que notei que solicitar o plano real adiciona um pouco de sobrecarga relativa neste estágio. Todas as outras otimizações possíveis que me vêm à mente não são seguras para produção, então vou parar por aqui.
Como pensamento final, você realmente deseja retornar 1.000 linhas arbitrárias e o nome mínimo da conferência? Essas informações são úteis para seus usuários finais?