Database
 sql >> Datenbank >  >> RDS >> Database

Aufrechterhaltung eines gruppierten laufenden MAX (oder MIN)

Hinweis:Dieser Beitrag wurde ursprünglich nur in unserem eBook High Performance Techniques for SQL Server, Band 3 veröffentlicht. Sie können sich hier über unsere eBooks informieren.

Eine Anforderung, die ich gelegentlich sehe, besteht darin, eine Abfrage mit nach Kunden gruppierten Bestellungen zurückzugeben, die die maximal fällige Summe für jede Bestellung bis heute anzeigt (ein "laufendes Maximum"). Stellen Sie sich also diese Beispielzeilen vor:

SalesOrderID Kundennummer Bestelldatum TotalDue
12 2 2014-01-01 37,55
23 1 2014-01-02 45.29
31 2 2014-01-03 24.56
32 2 2014-01-04 89.84
37 1 2014-01-05 32.56
44 2 6.1.2014 45.54
55 1 2014-01-07 99.24
62 2 2014-01-08 12.55

Einige Zeilen mit Beispieldaten

Die gewünschten Ergebnisse aus den angegebenen Anforderungen lauten wie folgt:Einfach ausgedrückt:Sortieren Sie die Bestellungen jedes Kunden nach Datum und listen Sie jede Bestellung auf. Wenn dies der höchste TotalDue-Wert für alle bis zu diesem Datum angezeigten Bestellungen ist, drucken Sie die Gesamtsumme dieser Bestellung aus, andernfalls drucken Sie den höchsten TotalDue-Wert aller vorherigen Bestellungen aus:

SalesOrderID Kundennummer Bestelldatum TotalDue MaxTotalDue
12 1 2014-01-02 45.29 45.29
23 1 2014-01-05 32.56 45.29
31 1 2014-01-07 99.24 99.24
32 2 2014-01-01 37,55 37,55
37 2 2014-01-03 24.56 37,55
44 2 2014-01-04 89.84 89.84
55 2 6.1.2014 45,54 89.84
62 2 2014-01-08 12.55 89.84

Beispiel für gewünschte Ergebnisse

Viele Leute würden instinktiv einen Cursor oder eine While-Schleife verwenden wollen, um dies zu erreichen, aber es gibt mehrere Ansätze, die diese Konstrukte nicht beinhalten.

Korrelierte Unterabfrage

Dieser Ansatz scheint der einfachste und unkomplizierteste Ansatz für das Problem zu sein, aber er hat sich immer wieder als nicht skalierbar erwiesen, da die Lesevorgänge exponentiell wachsen, wenn die Tabelle größer wird:

SELECT /* Correlated Subquery */ SalesOrderID, CustomerID, OrderDate, TotalDue,
  MaxTotalDue = (SELECT MAX(TotalDue) 
     FROM Sales.SalesOrderHeader
     WHERE CustomerID = h.CustomerID
     AND SalesOrderID <= h.SalesOrderID)
  FROM Sales.SalesOrderHeader AS h
  ORDER BY CustomerID, SalesOrderID;

Hier ist der Plan gegen AdventureWorks2014 mit SQL Sentry Plan Explorer:

Ausführungsplan für korrelierte Unterabfrage (zum Vergrößern klicken)

Selbstreferenzierende CROSS APPLY

Dieser Ansatz ist nahezu identisch mit dem Correlated Subquery-Ansatz in Bezug auf Syntax, Planform und Leistung im Maßstab.

SELECT /* CROSS APPLY */ h.SalesOrderID, h.CustomerID, h.OrderDate, h.TotalDue, x.MaxTotalDue
FROM Sales.SalesOrderHeader AS h
CROSS APPLY
(
  SELECT MaxTotalDue = MAX(TotalDue)
    FROM Sales.SalesOrderHeader AS i
    WHERE i.CustomerID = h.CustomerID
    AND i.SalesOrderID <= h.SalesOrderID
) AS x
ORDER BY h.CustomerID, h.SalesOrderID;

Der Plan ist dem korrelierten Unterabfrageplan ziemlich ähnlich, der einzige Unterschied besteht in der Position einer Art:

Ausführungsplan für CROSS APPLY (zum Vergrößern klicken)

Rekursiver CTE

Hinter den Kulissen verwendet dies Schleifen, aber bis wir es tatsächlich ausführen, können wir so tun, als ob es nicht so wäre (obwohl es mit Sicherheit das komplizierteste Stück Code ist, das ich jemals schreiben möchte, um dieses spezielle Problem zu lösen):

;WITH /* Recursive CTE */ cte AS 
(
  SELECT  SalesOrderID, CustomerID, OrderDate, TotalDue, MaxTotalDue 
    FROM 
	(
	  SELECT SalesOrderID, CustomerID, OrderDate, TotalDue, MaxTotalDue = TotalDue, 
	    rn = ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY SalesOrderID)
	  FROM Sales.SalesOrderHeader
	) AS x
  WHERE rn = 1
  UNION ALL
  SELECT r.SalesOrderID, r.CustomerID, r.OrderDate, r.TotalDue,
    MaxTotalDue = CASE 
	  WHEN r.TotalDue > cte.MaxTotalDue THEN r.TotalDue 
	  ELSE cte.MaxTotalDue 
	END
  FROM cte
  CROSS APPLY
  (
    SELECT  SalesOrderID, CustomerID, OrderDate, TotalDue,
      rn = ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY SalesOrderID)
    FROM Sales.SalesOrderHeader AS h
    WHERE h.CustomerID = cte.CustomerID
    AND h.SalesOrderID > cte.SalesOrderID
  ) AS r
  WHERE r.rn = 1
)
SELECT SalesOrderID, CustomerID, OrderDate, TotalDue, MaxTotalDue
FROM cte
ORDER BY CustomerID, SalesOrderID
OPTION (MAXRECURSION 0);

Sie können sofort sehen, dass der Plan komplexer ist als die beiden vorherigen, was angesichts der komplexeren Abfrage nicht verwundert:

Ausführungsplan für rekursiven CTE (zum Vergrößern klicken)

Aufgrund einiger schlechter Schätzungen sehen wir eine Indexsuche mit einer begleitenden Schlüsselsuche, die wahrscheinlich beide durch einen einzigen Scan hätten ersetzt werden sollen, und wir erhalten auch eine Sortieroperation, die letztendlich in tempdb übergehen muss (Sie können dies im Tooltip sehen wenn Sie den Mauszeiger über den Sortieroperator mit dem Warnsymbol bewegen):

MAX() OVER (ROWS UNBOUNDED)

Dies ist eine Lösung, die nur in SQL Server 2012 und höher verfügbar ist, da sie neu eingeführte Erweiterungen für Fensterfunktionen verwendet.

SELECT /* MAX() OVER() */ SalesOrderID, CustomerID, OrderDate, TotalDue,
  MaxTotalDue = MAX(TotalDue) OVER 
  (
    PARTITION BY CustomerID ORDER BY SalesOrderID 
    ROWS UNBOUNDED PRECEDING
  )
FROM Sales.SalesOrderHeader
ORDER BY CustomerID, SalesOrderID;

Der Plan zeigt genau, warum er besser skaliert als alle anderen; es hat nur eine geclusterte Index-Scan-Operation, im Gegensatz zu zwei (oder der schlechten Wahl eines Scans und einer Suche + Suche im Fall des rekursiven CTE):

Ausführungsplan für MAX() OVER() (zum Vergrößern klicken)

Leistungsvergleich

Die Pläne lassen uns sicherlich glauben, dass das neue MAX() OVER() Die Leistungsfähigkeit in SQL Server 2012 ist ein echter Gewinner, aber wie sieht es mit konkreten Laufzeitmetriken aus? So wurden die Hinrichtungen verglichen:

Die ersten beiden Abfragen waren fast identisch; während in diesem Fall das CROSS APPLY in Bezug auf die Gesamtdauer um einen kleinen Rand besser war, schlägt die korrelierte Unterabfrage stattdessen manchmal etwas. Der rekursive CTE ist jedes Mal wesentlich langsamer, und Sie können die Faktoren sehen, die dazu beitragen – nämlich die schlechten Schätzungen, die massive Menge an Lesevorgängen, die Schlüsselsuche und die zusätzliche Sortieroperation. Und wie ich zuvor mit laufenden Summen demonstriert habe, ist die SQL Server 2012-Lösung in fast jeder Hinsicht besser.

Schlussfolgerung

Wenn Sie SQL Server 2012 oder höher verwenden, möchten Sie sich auf jeden Fall mit allen Erweiterungen der Windowing-Funktionen vertraut machen, die erstmals in SQL Server 2005 eingeführt wurden – sie können Ihnen einige ziemlich ernsthafte Leistungssteigerungen geben, wenn Sie noch laufenden Code erneut aufrufen. der alte Weg." Wenn Sie mehr über einige dieser neuen Funktionen erfahren möchten, empfehle ich das Buch von Itzik Ben-Gan, Microsoft SQL Server 2012 High-Performance T-SQL Using Window Functions.

Wenn Sie noch nicht auf SQL Server 2012 sind, könnten Sie zumindest in diesem Test zwischen CROSS APPLY wählen und die korrelierte Unterabfrage. Wie immer sollten Sie verschiedene Methoden anhand Ihrer Daten auf Ihrer Hardware testen.