sql >> Databasteknik >  >> RDS >> Database

Upprätthålla ett grupperat löpande MAX (eller MIN)

Obs! Det här inlägget publicerades ursprungligen endast i vår e-bok, High Performance Techniques for SQL Server, Volym 3. Du kan ta reda på om våra e-böcker här.

Ett krav som jag ser ibland är att få en förfrågan returnerad med beställningar grupperade efter kund, som visar den maximala summan som förfaller för en beställning hittills (ett "löpande max"). Så föreställ dig dessa exempelrader:

Försäljningsorder-ID Kund-ID Beställningsdatum 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 2014-01-06 45,54
55 1 2014-01-07 99,24
62 2 2014-01-08 12.55

Några rader med exempeldata

De önskade resultaten från de angivna kraven är som följer – i klartext, sortera varje kunds beställningar efter datum och lista varje beställning. Om det är det högsta TotalDue-värdet för alla beställningar fram till det datumet, skriv ut den beställningens totalsumma, annars skriv ut det högsta TotalDue-värdet från alla tidigare beställningar:

Försäljningsorder-ID Kund-ID Beställningsdatum 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 2014-01-06 45,54 89,84
62 2 2014-01-08 12.55 89,84

Exempel på önskade resultat

Många människor skulle instinktivt vilja använda en markör eller while-loop för att åstadkomma detta, men det finns flera tillvägagångssätt som inte involverar dessa konstruktioner.

Korrelerad underfråga

Detta tillvägagångssätt verkar vara det enklaste och mest okomplicerade tillvägagångssättet till problemet, men det har bevisats gång på gång att det inte skalas, eftersom läsningarna växer exponentiellt när tabellen blir större:

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;

Här är planen mot AdventureWorks2014, med SQL Sentry Plan Explorer:

Utförandeplan för korrelerad underfråga (klicka för att förstora)

Självrefererande KORSA TILLÄMPNING

Detta tillvägagångssätt är nästan identiskt med Correlated Subquery-metoden, när det gäller syntax, planform och prestanda i skala.

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;

Planen är ganska lik den korrelerade underfrågeplanen, den enda skillnaden är platsen för en sort:

Exekutivplan för CROSS APPLY (klicka för att förstora)

Rekursiv CTE

Bakom kulisserna använder detta loopar, men tills vi faktiskt kör det kan vi liksom låtsas att det inte gör det (även om det lätt är den mest komplicerade kodbiten jag någonsin skulle vilja skriva för att lösa just det här problemet):

;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);

Du kan direkt se att planen är mer komplex än de två föregående, vilket inte är förvånande med tanke på den mer komplexa frågan:

Utförandeplan för rekursiv CTE (klicka för att förstora)

På grund av några dåliga uppskattningar ser vi en indexsökning med en åtföljande nyckelsökning som förmodligen både borde ha ersatts av en enda skanning, och vi får också en sorteringsoperation som i slutändan måste spillas ut till tempdb (du kan se detta i verktygstipset om du håller muspekaren över sorteringsoperatorn med varningsikonen):

MAX() ÖVER (RADER OBEGRÄNSADE)

Detta är en lösning endast tillgänglig i SQL Server 2012 och högre, eftersom den använder nyintroducerade tillägg till fönsterfunktioner.

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;

Planen visar exakt varför den skalar bättre än alla andra; den har bara en klustrad indexskanningsoperation, i motsats till två (eller det dåliga valet av en skanning och en sökning + uppslag i fallet med den rekursiva CTE):

Exekutivplan för MAX() OVER() (klicka för att förstora)

Prestandajämförelse

Planerna får oss verkligen att tro att den nya MAX() OVER() kapacitet i SQL Server 2012 är en riktig vinnare, men vad sägs om påtagliga körtidsmått? Så här jämfördes avrättningarna:

De två första frågorna var nästan identiska; medan i detta fall CROSS APPLY var bättre vad gäller den totala varaktigheten med en liten marginal, den korrelerade underfrågan slår den ibland lite istället. Den rekursiva CTE är avsevärt långsammare varje gång, och du kan se de faktorer som bidrar till det – nämligen de dåliga uppskattningarna, den enorma mängden avläsningar, nyckelsökningen och den extra sorteringsoperationen. Och som jag har visat tidigare med löpande summor är SQL Server 2012-lösningen bättre i nästan alla aspekter.

Slutsats

Om du använder SQL Server 2012 eller senare vill du definitivt bekanta dig med alla tillägg till fönsterfunktionerna som först introducerades i SQL Server 2005 – de kan ge dig några ganska allvarliga prestandahöjningar när du återvänder till kod som fortfarande körs " det gamla sättet." Om du vill lära dig mer om några av dessa nya funktioner rekommenderar jag starkt Itzik Ben-Gans bok, Microsoft SQL Server 2012 High-Performance T-SQL Using Window Functions.

Om du inte är på SQL Server 2012 ännu, åtminstone i detta test, kan du välja mellan CROSS APPLY och den korrelerade underfrågan. Som alltid bör du testa olika metoder mot dina data på din hårdvara.


  1. EM 12c Justering av tröskelvärden

  2. SQLite ingen sådan kolumn

  3. Få lista över alla bord i Oracle?

  4. Hur förser man en API-klient med 1 000 000 databasresultat?