Um exemplo dessa pergunta mostra que o SQL Server escolherá uma varredura de índice completa para resolver uma consulta como esta:
select distinct [typeName] from [types]
Onde [typeName] tem um índice ascendente não clusterizado e não exclusivo. Seu exemplo tem 200 milhões de linhas, mas apenas 76 valores únicos. Parece que um plano de busca seria uma escolha melhor com essa densidade (~ 76 pesquisas binárias múltiplas)?
O caso dele poderia ser normalizado mas o motivo da pergunta é que eu realmente quero resolver algo assim:
select TransactionId, max(CreatedUtc)
from TxLog
group by TransactionId
Existe um índice em (TransactionId, MaxCreatedUtc)
.
Reescrever usando uma fonte normalizada (dt) não altera o plano.
select dt.TransactionId, MaxCreatedUtc
from [Transaction] dt -- distinct transactions
cross apply
(
select Max(CreatedUtc) as MaxCreatedUtc
from TxLog tl
where tl.TransactionId = dt.TransactionId
) ca
Executar apenas a subconsulta CA como uma UDF escalar mostra um plano de 1 busca.
select max(CreatedUtc) as MaxCreatedUtc
from Pub.TransactionLog
where TransactionID = @TxId;
Usar esse UDF escalar na consulta original parece funcionar, mas perde o paralelismo (problema conhecido com UDFs):
select t.typeName,
Pub.ufn_TransactionMaxCreatedUtc(t.TransactionId) as MaxCreatedUtc
from Pub.[Transaction] t
Reescrever usando um TVF Inline o reverte para o plano baseado em varredura.
Da resposta/comentário @ypercube:
select TransactionId, MaxCreatedUtc
from Pub.[Transaction] t
cross apply
(
select top (1) CreatedUtc as MaxCreatedUtc
from Pub.TransactionLog l
where l.TransactionID = t.TransactionId
order by CreatedUtc desc
) ca
Plano parece bom. Sem paralelismo, mas inútil desde tão rápido. Terá que tentar isso em um problema maior em algum momento. Obrigado.
Eu tenho exatamente a mesma configuração e passei pelos mesmos estágios de reescrever a consulta.
No meu caso, os nomes e significados das tabelas são um pouco diferentes, mas a estrutura geral é a mesma. Sua tabela
Transactions
corresponde à minha tabelaPortalElevators
abaixo. Tem aproximadamente 2.000 linhas. Sua mesaTxLog
corresponde à minha mesaPlaybackStats
. Tem aproximadamente 150 milhões de linhas. Tem índice em(ElevatorID, DataSourceRowID)
, igual a você.Executarei várias variantes da consulta nos dados reais e compararei planos de execução, E/S e estatísticas de tempo. Estou usando o SQL Server 2008 Standard.
GROUP BY com MAX
O mesmo que para o seu otimizador verifica o índice e agrega os resultados. Lento.
linha individual
Vamos ver o que o otimizador faria se eu solicitasse
MAX
apenas uma linha:O Optimizer é inteligente o suficiente para usar o índice e faz uma busca. A propósito, podemos ver que o otimizador usa o
TOP
operador, mesmo que a consulta não o tenha. Este é um sinal claro de que os caminhos de otimização deMAX
eTOP
têm algo em comum no motor, mas são diferentes como veremos a seguir.CROSS APPLY com MAX
O Optimizer ainda verifica o índice. Não é inteligente o suficiente converter
MAX
eTOP
digitalizar em buscas aqui. Lento. Não pensei nessa variante originalmente, minha próxima tentativa foi UDF escalar.UDF escalar
Eu vi que o plano para obter
MAX
uma linha individual tinha busca de índice, então coloquei essa consulta simples em uma UDF escalar.Ele corre rápido. Pelo menos, muito mais rápido do que
Group by
. Infelizmente, o plano de execução não mostra os detalhes do UDF e, o que é ainda pior, não mostra as estatísticas reais do IO (não inclui o IO gerado pelo UDF). Você precisa executar o Profiler para ver todas as chamadas da função e suas estatísticas. Este plano mostra apenas 6 leituras. O plano para linha individual tem 4 leituras, então o número real seria próximo a:6 + 2779 * 4 = 6 + 11,116 = 11,122
.CROSS APPLY com TOP
Eventualmente, descobri o
CROSS APPLY
e como pode ser aplicado ;-) neste caso.Aqui, o otimizador é inteligente o suficiente para fazer aproximadamente 2.000 buscas. Você pode ver que o número de leituras é muito menor do que para
group by
. Velozes.Curiosamente, o número de leituras aqui (11.850) é um pouco mais do que as leituras que estimei com UDF (11.122). As estatísticas de IO da tabela
CROSS APPLY
têm 11.844 leituras e 2.779 contagens de varredura da tabela grande, o que fornece11,844 / 2,779 ~= 4.26
leituras por busca de índice. Muito provavelmente, as buscas para alguns valores usam 4 leituras e para outros 5, com média de 4,26. Existem 2.779 buscas, mas há valores apenas para 2.130 linhas. Como eu disse, é difícil obter um número real de leituras com UDF sem o profiler.CTE recursivo
Como foi apontado nos comentários, Paul White descreveu um método Recursive Index Skip Scan para encontrar valores distintos em uma tabela grande sem executar uma varredura de índice completa, mas fazer buscas de índice recursivamente. Para iniciar a recursão, precisamos encontrar o valor
MIN
ouMAX
para uma âncora e, em seguida, cada etapa da recursão adiciona o próximo valor, um por um. O post explica em detalhes.É bastante rápido, embora execute quase o dobro da quantidade de leituras do
CROSS APPLY
. Faz 12.781 leiturasWorktable
e 8.524 emPlaybackStats
. Por outro lado, ele realiza tantas buscas quantos forem os valores distintos na tabela grande.CROSS APPLY
withTOP
executa tantas buscas quanto as linhas da pequena tabela. No meu caso, a tabela pequena possui 2.779 linhas, mas a tabela grande possui apenas 2.130 valores distintos.Resumo
Executei cada consulta três vezes e escolhi o melhor momento. Não houve leituras físicas.
Conclusão
Neste caso especial de
greatest-n-per-group
problema temos:n=1
;Dois melhores métodos são:
Caso tenhamos uma pequena tabela com a lista de grupos, o melhor método é
CROSS APPLY
comTOP
.No caso de termos apenas uma tabela grande, o melhor método é
Recursive Index Skip Scan
.