Mise à jour 2014-12-18
Avec la réponse écrasante à la question principale étant "Non", les réponses les plus intéressantes se sont concentrées sur la partie 2, comment résoudre le puzzle de la performance avec une explicite ORDER BY
. Bien que j'aie déjà marqué une réponse, je ne serais pas surpris s'il existait une solution encore plus performante.
Original
Cette question s'est posée parce que la seule solution extrêmement rapide que j'ai pu trouver à un problème particulier ne fonctionne que sans ORDER BY
clause. Vous trouverez ci-dessous l'intégralité du T-SQL nécessaire pour produire le problème, ainsi que ma solution proposée (j'utilise SQL Server 2008 R2, si cela importe.)
--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
Voici les plans d'exécution de la solution A et B, respectivement:
La solution A donne les performances dont j'ai besoin, mais je n'ai pas pu les faire fonctionner avec les mêmes performances lors de l'ajout d'une clause ORDER BY (par exemple, voir la solution B). Et il semble certainement que la solution A devrait fournir ses résultats dans l'ordre, car 1) la table n'a qu'un seul index, 2) une recherche est forcée, éliminant ainsi la possibilité d'utiliser une analyse de l'ordre d'allocation basée sur les pages IAM .
Mes questions sont donc:
Ai-je raison de dire qu'il garantira la commande dans ce cas sans clause de commande par clause?
Sinon, existe-t-il une autre méthode pour forcer un plan aussi rapide que la solution A, de préférence une méthode qui évite les tris? Notez qu'il devrait résoudre exactement le même problème (pour
StoreID = 1
, trouver les 500 meilleurs clients commandés par leur montant d'achat le plus cher). Il faudrait également utiliser la#Orders
table, mais différents schémas d'indexation seraient corrects.
la source
ORDER BY
.Réponses:
Non. Un flux distinct qui préserve l'ordre (permettant
ORDER BY
sans tri) n'est pas implémenté dans SQL Server aujourd'hui. Il est possible de le faire en principe, mais alors beaucoup de choses sont possibles si nous sommes autorisés à changer le code source de SQL Server. Si vous pouvez faire un bon argumentaire pour ce travail de développement, vous pouvez le suggérer à Microsoft .Oui.(Les indices de table et de requête sont requis uniquement lors de l'utilisation de l'estimateur de cardinalité antérieur à 2014):
Solution SQL CLR
Le script suivant montre l'utilisation d'une fonction table SQL CLR pour répondre aux exigences indiquées. Je ne suis pas un expert C #, donc le code peut être amélioré:
Table de test et échantillon de données de la question:
Test de fonctionnalité:
Plan d'exécution (notez la validation du
ORDER
garantie):Sur mon ordinateur portable, cela s'exécute généralement en 80-100 ms. C'est loin d'être aussi rapide que la réécriture T-SQL ci-dessus, mais cela devrait montrer une bonne stabilité des performances face aux différentes distributions de données.
Code source:
la source
Sans
ORDER BY
beaucoup de choses peuvent mal tourner. Vous avez exclu tous les problèmes possibles auxquels je peux penser, mais cela ne signifie pas qu'il n'y a pas de problème et qu'il n'y en aura pas dans une prochaine version.Cela devrait fonctionner:
Tirez des lots de 500 lignes de la table en boucle et arrêtez-vous lorsque vous disposez de 500 ID client distincts. La requête d'extraction pourrait ressembler à ceci:
Cela effectuera une analyse de plage ordonnée sur l'index. Le
Amount <= @lastAmountFetched
prédicat est là pour extraire progressivement plus d'enregistrements. Chaque requête ne touchera physiquement que 500 enregistrements. Cela signifie que c'est O (1). Il ne devient pas plus cher à mesure que vous entrez dans l'index.Vous devez conserver la variable
@lastAmountFetched
pour réduire à la plus petite valeur que vous avez récupérée dans cette instruction.De cette façon, vous analyserez progressivement l'index de manière ordonnée. Vous lirez au plus (500 - 1) lignes de plus que la quantité optimale aurait été.
Ce sera beaucoup plus rapide que de toujours regrouper environ 100 000 commandes pour un magasin particulier. Probablement, seules quelques itérations de 500 lignes chacune seront nécessaires.
Il s'agit essentiellement d'un opérateur distinct de flux codé manuellement.
Vous pouvez également utiliser un curseur pour extraire le moins de lignes possible. Cela sera beaucoup plus lent car l'exécution de 500 requêtes sur une seule ligne est le plus souvent plus lente que l'exécution d'un lot de 500 lignes.
Vous pouvez également simplement interroger toutes les lignes sans
DISTINCT
de manière ordonnée et faire en sorte que l'application cliente termine la requête une fois que suffisamment de lignes ont été renvoyées (en utilisantSqlCommand.Cancel
).la source
#fetchedOrders
qu'il ne contient pas de clients que nous avons déjà vus? On peut supposer que cela implique un indice chercher sur la table temporaire, ce qui est pas tout à fait la même chose comme un « flux distinct » et ne se plus cher plus de lignes que nous avons vu (bien qu'il encore battre la solution B à tous , mais le pire des cas d'avoir à scanner toutes les lignes car il n'y a qu'un seul client, pour lequel A et B fonctionneront de manière identique).IGNORE_DUP_KEY
pourrait le faire.