Update 18.12.2014
Mit der überwältigenden Antwort auf die Hauptfrage "Nein" haben sich die interessanteren Antworten auf Teil 2 konzentriert, wie das Leistungsrätsel mit einem expliziten gelöst werden kann ORDER BY
. Obwohl ich bereits eine Antwort markiert habe, wäre ich nicht überrascht, wenn es eine noch leistungsstärkere Lösung gäbe.
Original
Diese Frage stellte sich, weil die einzige extrem schnelle Lösung, die ich für ein bestimmtes Problem finden konnte, nur ohne ORDER BY
Klausel funktioniert . Im Folgenden finden Sie die vollständige T-SQL, die zur Behebung des Problems benötigt wird, sowie meine vorgeschlagene Lösung (ich verwende SQL Server 2008 R2, falls dies von Bedeutung ist.)
--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
Hier sind die Ausführungspläne für Lösung A bzw. B:
Lösung A liefert die Leistung, die ich benötige, aber ich konnte sie beim Hinzufügen einer ORDER BY-Klausel nicht mit der gleichen Leistung zum Laufen bringen (siehe z. B. Lösung B). Und es scheint, als müsste Lösung A ihre Ergebnisse in der richtigen Reihenfolge liefern, da 1) die Tabelle nur einen Index enthält, 2) eine Suche erzwungen wird, wodurch die Möglichkeit der Verwendung eines Scans der Zuordnungsreihenfolge basierend auf IAM-Seiten ausgeschlossen ist .
Meine Fragen sind also:
Habe ich Recht, dass es in diesem Fall die Bestellung ohne eine Bestellung per Klausel garantiert?
Wenn nicht, gibt es eine andere Methode, um einen Plan zu erzwingen, der so schnell ist wie Lösung A, vorzugsweise einen Plan, der Sortierungen vermeidet? Beachten Sie, dass es genau dasselbe Problem lösen müsste (z
StoreID = 1
. B. finden Sie die Top-500-Kunden, die nach ihrem teuersten Kaufbetrag bestellt wurden). Es müsste auch noch die#Orders
Tabelle verwenden, aber andere Indizierungsschemata wären in Ordnung.
quelle
ORDER BY
.Antworten:
Nein. Ein Flow Distinct , der die Reihenfolge beibehält (
ORDER BY
ohne Sortierung zulässt), ist heute in SQL Server nicht implementiert. Das ist im Prinzip möglich, aber dann sind viele Dinge möglich, wenn wir den SQL Server-Quellcode ändern dürfen. Wenn Sie ein gutes Argument für diese Entwicklungsarbeit abgeben können, können Sie es Microsoft vorschlagen .Ja. (Tabellen- und Abfragehinweise nur erforderlich, wenn der Kardinalitätsschätzer vor 2014 verwendet wird):
SQL CLR-Lösung
Das folgende Skript zeigt die Verwendung einer SQL CLR-Tabellenwertfunktion, um die angegebenen Anforderungen zu erfüllen. Ich bin kein C # -Experte, daher kann der Code verbessert werden:
Testtabelle und Beispieldaten aus der Frage:
Funktionstest:
Ausführungsplan (Validierung der
ORDER
Garantie beachten ):Auf meinem Laptop wird dies normalerweise in 80-100 ms ausgeführt. Dies ist bei weitem nicht so schnell wie das oben beschriebene Umschreiben von T-SQL, es sollte jedoch trotz unterschiedlicher Datenverteilungen eine gute Leistungsstabilität aufweisen.
Quellcode:
quelle
Ohne
ORDER BY
viele Dinge kann es schief gehen. Sie haben alle möglichen Probleme ausgeschlossen, die mir einfallen, aber das bedeutet nicht, dass es kein Problem gibt, und es wird keines in einer zukünftigen Version geben.Das sollte funktionieren:
Ziehen Sie in einer Schleife Stapel mit 500 Zeilen aus der Tabelle und halten Sie an, wenn Sie 500 verschiedene Kunden-IDs haben. Die Abrufabfrage könnte folgendermaßen aussehen:
Dadurch wird ein geordneter Bereichsscan für den Index durchgeführt. Das
Amount <= @lastAmountFetched
Prädikat dient zum schrittweisen Abrufen weiterer Datensätze. Jede Abfrage berührt nur 500 Datensätze. Das heißt, es ist O (1). Je weiter Sie in den Index einsteigen, desto teurer wird es nicht.Sie müssen die Variable pflegen
@lastAmountFetched
, um sie auf den kleinsten Wert zu verringern, den Sie in dieser Anweisung abgerufen haben.Auf diese Weise scannen Sie den Index schrittweise in geordneter Reihenfolge. Sie werden höchstens (500 - 1) Zeilen mehr lesen, als die optimale Menge gewesen wäre.
Dies ist viel schneller, als immer 100000 Bestellungen für ein bestimmtes Geschäft zusammenzufassen. Wahrscheinlich werden nur wenige Iterationen mit jeweils 500 Zeilen benötigt.
Im Wesentlichen handelt es sich hierbei um einen manuell codierten Flow-Different-Operator.
Verwenden Sie alternativ einen Cursor, um so wenige Zeilen wie möglich abzurufen. Dies ist viel langsamer, da die Ausführung von 500 Einzelzeilenabfragen am häufigsten langsamer ist als die Ausführung eines Stapels von 500 Zeilen.
Alternativ fragen Sie einfach alle Zeilen ohne
DISTINCT
in einer geordneten Weise ab und veranlassen die Client-Anwendung, die Abfrage zu beenden, sobald genügend Zeilen zurückgegeben wurden (mithilfe vonSqlCommand.Cancel
).quelle
#fetchedOrders
es keine Kunden gibt, die wir bereits gesehen haben? Vermutlich handelt ein Index für die temporäre Tabelle suchen, die nicht ganz dasselbe wie ein „fließen distinct“ ist und nicht bekommen teurer desto mehr Zeilen die wir gesehen haben (obwohl es immer noch die Lösung B in allen , aber den schlimmsten Fall schlagen alle Zeilen scannen zu müssen, weil es nur einen Kunden gibt, für den A und B identisch abschneiden).IGNORE_DUP_KEY
könnte das tun.