Tenho uma tabela assim:
CREATE TABLE Updates
(
UpdateId INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
ObjectId INT NOT NULL
)
Essencialmente, rastreando atualizações de objetos com um ID crescente.
O consumidor desta tabela selecionará um bloco de 100 IDs de objetos distintos, ordenados por UpdateId
e a partir de um UpdateId
. Essencialmente, mantendo o controle de onde parou e, em seguida, consultando as atualizações.
Descobri que isso é um problema de otimização interessante porque só consegui gerar um plano de consulta maximamente ideal escrevendo consultas que fazem o que eu quero devido aos índices, mas não garantem o que eu quero:
SELECT DISTINCT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId
Onde @fromUpdateId
é um parâmetro de procedimento armazenado.
Com um plano de:
SELECT <- TOP <- Hash match (flow distinct, 100 rows touched) <- Index seek
Devido à busca no UpdateId
índice que está sendo usado, os resultados já são bons e ordenados do menor para o maior ID de atualização, como eu quero. E isso gera um plano de fluxo distinto , que é o que eu quero. Mas o ordenamento obviamente não é um comportamento garantido, então não quero usá-lo.
Esse truque também resulta no mesmo plano de consulta (embora com um TOP redundante):
WITH ids AS
(
SELECT ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId
ORDER BY UpdateId OFFSET 0 ROWS
)
SELECT DISTINCT TOP 100 ObjectId FROM ids
No entanto, não tenho certeza (e suspeito que não) se isso realmente garante o pedido.
Uma consulta que eu esperava que o SQL Server fosse inteligente o suficiente para simplificar era essa, mas acaba gerando um plano de consulta muito ruim:
SELECT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId
GROUP BY ObjectId
ORDER BY MIN(UpdateId)
Com um plano de:
SELECT <- Top N Sort <- Hash Match aggregate (50,000+ rows touched) <- Index Seek
Estou tentando encontrar uma maneira de gerar um plano ideal com uma busca de índice ativada UpdateId
e um fluxo distinto para remover ObjectId
s duplicados. Alguma ideia?
Dados de amostra, se você quiser. Os objetos raramente terão mais de uma atualização e quase nunca devem ter mais de uma em um conjunto de 100 linhas, e é por isso que estou atrás de um fluxo distinct , a menos que haja algo melhor que eu não conheça? No entanto, não há garantia de que um único ObjectId
não terá mais de 100 linhas na tabela. A tabela tem mais de 1.000.000 de linhas e espera-se que cresça rapidamente.
Suponha que o usuário deste tenha outra maneira de encontrar o próximo apropriado @fromUpdateId
. Não há necessidade de devolvê-lo nesta consulta.
O otimizador do SQL Server não pode produzir o plano de execução desejado com a garantia necessária, porque o operador Hash Match Flow Distinct não preserva a ordem.
Você pode observar a preservação da ordem em muitos casos, mas este é um detalhe de implementação; não há garantia, então você não pode confiar nele. Como sempre, a ordem de apresentação só pode ser garantida por uma
ORDER BY
cláusula de nível superior.Exemplo
O script abaixo mostra que Hash Match Flow Distinct não preserva a ordem. Ele configura a tabela em questão com números correspondentes de 1 a 50.000 em ambas as colunas:
A consulta de teste é:
O plano estimado mostra um índice de busca e fluxo distinto:
A saída certamente parece ordenada para começar:
...mas os valores mais abaixo começam a 'faltar':
...e eventualmente:
A explicação, neste caso específico, é que o operador de hash derrama:
Uma vez que uma partição se espalha, todas as linhas que fazem hash para a mesma partição também se espalham. As partições derramadas são processadas posteriormente, quebrando a expectativa de que valores distintos encontrados sejam emitidos imediatamente na sequência em que são recebidos.
Há muitas maneiras de escrever uma consulta eficiente para produzir o resultado ordenado que você deseja, como recursão ou usando um cursor. No entanto, isso não pode ser feito usando Hash Match Flow Distinct .
Estou insatisfeito com esta resposta porque não consegui obter um operador distinto de fluxo junto com resultados que eram garantidos como corretos. No entanto, tenho uma alternativa que deve obter um bom desempenho junto com resultados corretos. Infelizmente, isso requer que um índice não clusterizado seja criado na tabela.
Abordei esse problema tentando pensar em uma combinação de colunas que pudesse
ORDER BY
e obter os resultados corretos depois de aplicarDISTINCT
a elas. O valor mínimo deUpdateId
perObjectId
junto comObjectId
é uma dessas combinações. No entanto, pedir diretamente o mínimoUpdateId
parece resultar na leitura de todas as linhas da tabela. Em vez disso, podemos solicitar indiretamente o valor mínimo deUpdateId
com outra junção à tabela. A ideia é varrer aUpdates
tabela em ordem, descartar todas as linhas para as quaisUpdateId
não é o valor mínimo para essa linhaObjectId
e manter as primeiras 100 linhas. Com base em sua descrição da distribuição de dados, não deveríamos precisar descartar muitas linhas.Para preparação de dados, coloquei 1 milhão de linhas em uma tabela com 2 linhas para cada ObjectId distinto:
O índice não clusterizado em
Objectid
eUpdateId
é importante. Ele nos permite descartar com eficiência linhas que não têm o mínimoUpdateId
porObjectid
. Há muitas maneiras de escrever uma consulta que corresponda à descrição acima. Aqui está uma maneira usandoNOT EXISTS
:Aqui está uma imagem do plano de consulta :
Na melhor das hipóteses, o SQL Server fará apenas 100 buscas de índice em relação ao índice não clusterizado. Para simular ter muito azar eu mudei a consulta para retornar as primeiras 5000 linhas para o cliente. Isso resultou em 9.999 buscas de índice, então é como obter uma média de 100 linhas por
ObjectId
. Aqui está a saída deSET STATISTICS IO, TIME ON
:Eu amo a pergunta - Flow Distinct é um dos meus operadores favoritos.
Agora, a garantia é o problema. Quando você pensa no operador FD puxando linhas do operador Seek de forma ordenada, produzindo cada linha conforme determina que ela seja única, isso lhe dará as linhas na ordem correta. Mas é difícil saber se pode haver alguns cenários em que o FD não lida com uma única linha de cada vez.
Teoricamente, o FD poderia solicitar 100 linhas do Seek e produzi-las na ordem que precisar.
As dicas de consulta
OPTION (FAST 1, MAXDOP 1)
podem ajudar, porque evitarão obter mais linhas do que o necessário do operador Seek. Mas é uma garantia ? Não exatamente. Ele ainda pode decidir puxar uma página de linhas de cada vez, ou algo assim.Acho que com
OPTION (FAST 1, MAXDOP 1)
, suaOFFSET
versão lhe daria muita confiança sobre o pedido, mas não é uma garantia.