Atualização 18/12/2014
Com a resposta esmagadora à pergunta principal sendo "Não", as respostas mais interessantes se concentraram na parte 2, como resolver o quebra-cabeça do desempenho de forma explícita ORDER BY
. Embora eu já tenha marcado uma resposta, não ficaria surpreso se houvesse uma solução com desempenho ainda melhor.
Original
Essa questão surgiu porque a única solução extremamente rápida que eu poderia encontrar para um problema em particular só funciona sem uma ORDER BY
cláusula. Abaixo está o T-SQL completo necessário para produzir o problema, juntamente com a minha solução proposta (estou usando o SQL Server 2008 R2, se isso for importante).
--Create Orders table
IF OBJECT_ID('tempdb..#Orders') IS NOT NULL DROP TABLE #Orders
CREATE TABLE #Orders
(
OrderID INT NOT NULL IDENTITY(1,1)
, CustID INT NOT NULL
, StoreID INT NOT NULL
, Amount FLOAT NOT NULL
)
CREATE CLUSTERED INDEX IX ON #Orders (StoreID, Amount DESC, CustID)
--Add 1 million rows w/ 100K Customers each of whom had 10 orders
;WITH
Cte0 AS (SELECT 1 AS C UNION ALL SELECT 1), --2 rows
Cte1 AS (SELECT 1 AS C FROM Cte0 AS A, Cte0 AS B),--4 rows
Cte2 AS (SELECT 1 AS C FROM Cte1 AS A ,Cte1 AS B),--16 rows
Cte3 AS (SELECT 1 AS C FROM Cte2 AS A ,Cte2 AS B),--256 rows
Cte4 AS (SELECT 1 AS C FROM Cte3 AS A ,Cte3 AS B),--65536 rows
Cte5 AS (SELECT 1 AS C FROM Cte4 AS A ,Cte2 AS B),--1048576 rows
FinalCte AS (SELECT ROW_NUMBER() OVER (ORDER BY C) AS Number FROM Cte5)
INSERT INTO #Orders (CustID, StoreID, Amount)
SELECT CustID = Number / 10
, StoreID = Number % 4
, Amount = 1000 * RAND(Number)
FROM FinalCte
WHERE Number <= 1000000
SET STATISTICS IO ON
SET STATISTICS TIME ON
--For StoreID = 1, find the top 500 customers ordered by their most expensive purchase (Amount)
--Solution A: Without ORDER BY
DECLARE @Top INT = 500
SELECT DISTINCT TOP (@Top) CustID
FROM #Orders WITH(FORCESEEK)
WHERE StoreID = 1
OPTION(OPTIMIZE FOR (@Top = 1), FAST 1);
--9 logical reads, CPU Time = 0 ms, elapsed time = 1 ms
GO
--Solution B: With ORDER BY
DECLARE @Top INT = 500
SELECT TOP (@Top) CustID
FROM #Orders
WHERE StoreID = 1
GROUP BY CustID
ORDER BY MAX(Amount) DESC
OPTION(MAXDOP 1)
--745 logical reads, CPU Time = 141 ms, elapsed time = 145 ms
--Uses Sort operator
GO
Aqui estão os planos de execução para as soluções A e B, respectivamente:
A solução A fornece o desempenho necessário, mas não consegui fazê-lo funcionar com o mesmo desempenho ao adicionar qualquer cláusula ORDER BY (por exemplo, consulte a Solução B). E certamente parece que a Solução A teria que entregar seus resultados em ordem, pois 1) a tabela possui apenas um índice, 2) uma busca é forçada, eliminando assim a possibilidade de usar uma verificação de ordem de alocação com base nas páginas do IAM .
Então, minhas perguntas são:
Estou certo de que garantirá a ordem neste caso sem uma ordem por cláusula?
Caso contrário, existe outro método para forçar um plano tão rápido quanto a Solução A, preferencialmente um método que evite classificações? Observe que ele precisaria resolver exatamente o mesmo problema (para
StoreID = 1
encontrar os 500 principais clientes pedidos pelo valor de compra mais caro). Também seria necessário usar a#Orders
tabela, mas diferentes esquemas de indexação seriam aceitáveis.
fonte
ORDER BY
.Respostas:
Não. Um fluxo distinto que preserva a ordem (permitindo
ORDER BY
sem classificação) não está implementado no SQL Server hoje. É possível fazer isso em princípio, mas muitas coisas serão possíveis se tivermos permissão para alterar o código-fonte do SQL Server. Se você pode defender esse trabalho de desenvolvimento, sugira-o à Microsoft .Sim. (Dicas de tabela e consulta necessárias apenas ao usar o estimador de cardinalidade anterior a 2014):
Solução SQL CLR
O script a seguir mostra o uso de uma função com valor de tabela do SQL CLR para atender aos requisitos declarados. Como eu não sou especialista em C #, o código pode ter melhorias:
Tabela de teste e dados de amostra da pergunta:
Teste de funcionamento:
Plano de execução (observe a validação da
ORDER
garantia):No meu laptop, isso geralmente é executado em 80-100ms. Isso não chega nem perto da velocidade da reescrita do T-SQL acima, mas deve mostrar boa estabilidade de desempenho diante das diferentes distribuições de dados.
Código fonte:
fonte
Sem
ORDER BY
muitas coisas podem dar errado. Você excluiu todos os problemas possíveis em que consigo pensar, mas isso não significa que não há nenhum problema nem haverá um em uma versão futura.Isso deve funcionar:
Puxe lotes de 500 linhas da tabela em um loop e pare quando tiver 500 IDs de clientes distintos. A consulta de busca pode ficar assim:
Isso executará uma verificação de intervalo ordenada no índice. O
Amount <= @lastAmountFetched
predicado está lá para extrair incrementalmente mais registros. Cada consulta toca apenas fisicamente em 500 registros. Isso significa que é O (1). Não fica mais caro quanto mais você entra no índice.É necessário manter a variável
@lastAmountFetched
para diminuir para o menor valor que você buscou nessa instrução.Dessa forma, você fará a varredura incremental do índice de maneira ordenada. Você lerá no máximo (500 - 1) linhas mais do que a quantidade ideal seria.
Isso será muito mais rápido do que sempre agregar mais de 100000 pedidos para uma loja específica. Provavelmente, apenas algumas iterações de 500 linhas cada serão necessárias.
Essencialmente, este é um operador distinto de fluxo codificado manualmente.
Como alternativa, use um cursor para buscar o menor número possível de linhas. Isso será muito mais lento porque a execução de 500 consultas de linha única na maioria das vezes é mais lenta do que a execução de um lote de 500 linhas.
Como alternativa, basta consultar todas as linhas sem
DISTINCT
uma ordem ordenada e fazer com que o aplicativo cliente encerre a consulta assim que retornar linhas suficientes (usandoSqlCommand.Cancel
).fonte
#fetchedOrders
que não contenha clientes que já vimos? Presumivelmente, isso envolve uma busca de índice na tabela temporária, que não é exatamente a mesma coisa que um "fluxo distinta" e não ficar mais caros os mais linhas que vimos (embora ainda vai bater solução B em todos, mas o pior caso de ter que verificar todas as linhas porque há apenas um cliente, para o qual A e B terão desempenho idêntico).IGNORE_DUP_KEY
poderia fazer isso.