Atualização 2014-12-18
Com a resposta esmagadora à pergunta principal sendo "Não", as respostas mais interessantes se concentraram na parte 2, como resolver o quebra-cabeça de desempenho com um ORDER BY
. Embora eu já tenha marcado uma resposta, não ficaria surpreso se houvesse uma solução com desempenho ainda melhor.
Original
Essa dúvida surgiu porque a única solução extremamente rápida que consegui encontrar para um determinado problema só funciona sem uma ORDER BY
cláusula. Abaixo está o T-SQL completo necessário para produzir o problema, junto com 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 a Solução A e B, respectivamente:
A solução A fornece o desempenho de que preciso, mas não consegui fazê-la funcionar com o mesmo desempenho ao adicionar qualquer tipo de 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, já que 1) a tabela tem apenas um índice nela, 2) uma busca é forçada, eliminando assim a possibilidade de usar uma varredura de ordem de alocação baseada em páginas IAM .
Então minhas perguntas são:
Estou certo de que garantirá o pedido neste caso sem uma cláusula de pedido?
Caso contrário, existe outro método para forçar um plano que seja tão rápido quanto a Solução A, preferencialmente um que evite classificações? Observe que ele teria que resolver exatamente o mesmo problema (para
StoreID = 1
, encontre os 500 principais clientes solicitados pelo valor de compra mais caro). Também teria que usar a#Orders
tabela, mas diferentes esquemas de indexação seriam OK.
Não. Um Flow Distinct que preserva a ordem (permitindo
ORDER BY
sem uma classificação) não é implementado no SQL Server atualmente. É possível fazer isso em princípio, mas muitas coisas são possíveis se pudermos alterar o código-fonte do SQL Server. Se você pode fazer um bom caso para este trabalho de desenvolvimento, você pode sugerir isso para a Microsoft .Sim. (As dicas de tabela e consulta são 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 SQL CLR para atender aos requisitos declarados. Não sou especialista em C#, então o código pode ser melhorado:
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 normalmente é executado em 80-100ms. Isso não é nem de longe tão rápido quanto a reescrita do T-SQL acima, mas deve mostrar boa estabilidade de desempenho diante de diferentes distribuições de dados.
Código fonte:
Sem
ORDER BY
um monte de coisas podem dar errado. Você excluiu todos os problemas possíveis em que posso pensar, mas isso não significa que não haja problemas 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 ser assim:
Isso executará uma varredura de intervalo ordenada no índice. O
Amount <= @lastAmountFetched
predicado existe para obter mais registros de forma incremental. Cada consulta tocará fisicamente apenas 500 registros. Isso significa que é O(1). Não se torna mais caro quanto mais você avança no índice.Você tem que manter a variável
@lastAmountFetched
para diminuir para o menor valor que você buscou nessa instrução.Dessa forma, você verificará incrementalmente o índice de maneira ordenada. Você lerá no máximo (500 - 1) linhas a mais do que a quantidade ideal.
Isso será muito mais rápido do que sempre agregar cerca de 100.000 pedidos para uma loja específica. Provavelmente, serão necessárias apenas algumas iterações de 500 linhas cada.
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 é mais lenta do que a execução de um lote de 500 linhas.
Como alternativa, simplesmente consulte todas as linhas
DISTINCT
de maneira ordenada e faça com que o aplicativo cliente encerre a consulta assim que linhas suficientes forem retornadas (usandoSqlCommand.Cancel
).