sql >> Databasteknik >  >> RDS >> Sqlserver

Beräkna löpande total / löpande balans

För de som inte använder SQL Server 2012 eller senare är en markör förmodligen den mest effektiva som stöds och garanterat metod utanför CLR. Det finns andra tillvägagångssätt som den "udda uppdateringen" som kan vara marginellt snabbare men inte garanterad att fungera i framtiden, och naturligtvis set-baserade tillvägagångssätt med hyperboliska prestandaprofiler när tabellen blir större, och rekursiva CTE-metoder som ofta kräver direkt #tempdb I/O eller resultera i spill som ger ungefär samma effekt.

INNER JOIN - gör inte detta:

Den långsamma, uppsättningsbaserade metoden är av formen:

SELECT t1.TID, t1.amt, RunningTotal = SUM(t2.amt)
FROM dbo.Transactions AS t1
INNER JOIN dbo.Transactions AS t2
  ON t1.TID >= t2.TID
GROUP BY t1.TID, t1.amt
ORDER BY t1.TID;

Anledningen till att detta är långsamt? När tabellen blir större kräver varje inkrementell rad läsning av n-1 rader i tabellen. Detta är exponentiellt och bundet till misslyckanden, timeouts eller bara arga användare.

Korrelerad underfråga - gör inte detta heller:

Undersökningsformuläret är lika smärtsamt av liknande smärtsamma skäl.

SELECT TID, amt, RunningTotal = amt + COALESCE(
(
  SELECT SUM(amt)
    FROM dbo.Transactions AS i
    WHERE i.TID < o.TID), 0
)
FROM dbo.Transactions AS o
ORDER BY TID;

Konstig uppdatering - gör detta på egen risk:

Metoden "quirky update" är mer effektiv än ovanstående, men beteendet är inte dokumenterat, det finns inga garantier för ordning och beteendet kan fungera idag men kan gå sönder i framtiden. Jag inkluderar detta eftersom det är en populär metod och den är effektiv, men det betyder inte att jag stöder den. Den främsta anledningen till att jag till och med svarade på den här frågan istället för att stänga den som en dubblett är att den andra frågan har en udda uppdatering som accepterat svar.

DECLARE @t TABLE
(
  TID INT PRIMARY KEY,
  amt INT,
  RunningTotal INT
);
 
DECLARE @RunningTotal INT = 0;
 
INSERT @t(TID, amt, RunningTotal)
  SELECT TID, amt, RunningTotal = 0
  FROM dbo.Transactions
  ORDER BY TID;
 
UPDATE @t
  SET @RunningTotal = RunningTotal = @RunningTotal + amt
  FROM @t;
 
SELECT TID, amt, RunningTotal
  FROM @t
  ORDER BY TID;

Rekursiva CTE

Den här första förlitar sig på att TID är sammanhängande, inga luckor:

;WITH x AS
(
  SELECT TID, amt, RunningTotal = amt
    FROM dbo.Transactions
    WHERE TID = 1
  UNION ALL
  SELECT y.TID, y.amt, x.RunningTotal + y.amt
   FROM x 
   INNER JOIN dbo.Transactions AS y
   ON y.TID = x.TID + 1
)
SELECT TID, amt, RunningTotal
  FROM x
  ORDER BY TID
  OPTION (MAXRECURSION 10000);

Om du inte kan lita på detta kan du använda den här varianten, som helt enkelt bygger en sammanhängande sekvens med ROW_NUMBER() :

;WITH y AS 
(
  SELECT TID, amt, rn = ROW_NUMBER() OVER (ORDER BY TID)
    FROM dbo.Transactions
), x AS
(
    SELECT TID, rn, amt, rt = amt
      FROM y
      WHERE rn = 1
    UNION ALL
    SELECT y.TID, y.rn, y.amt, x.rt + y.amt
      FROM x INNER JOIN y
      ON y.rn = x.rn + 1
)
SELECT TID, amt, RunningTotal = rt
  FROM x
  ORDER BY x.rn
  OPTION (MAXRECURSION 10000);

Beroende på storleken på data (t.ex. kolumner som vi inte känner till), kan du hitta bättre övergripande prestanda genom att bara fylla de relevanta kolumnerna i en #temp-tabell först, och bearbeta mot den istället för bastabellen:

CREATE TABLE #x
(
  rn  INT PRIMARY KEY,
  TID INT,
  amt INT
);

INSERT INTO #x (rn, TID, amt)
SELECT ROW_NUMBER() OVER (ORDER BY TID),
  TID, amt
FROM dbo.Transactions;

;WITH x AS
(
  SELECT TID, rn, amt, rt = amt
    FROM #x
    WHERE rn = 1
  UNION ALL
  SELECT y.TID, y.rn, y.amt, x.rt + y.amt
    FROM x INNER JOIN #x AS y
    ON y.rn = x.rn + 1
)
SELECT TID, amt, RunningTotal = rt
  FROM x
  ORDER BY TID
  OPTION (MAXRECURSION 10000);

DROP TABLE #x;

Endast den första CTE-metoden kommer att ge prestanda som konkurrerar med den udda uppdateringen, men den gör ett stort antagande om datas natur (inga luckor). De andra två metoderna kommer att falla tillbaka och i de fallen kan du lika gärna använda en markör (om du inte kan använda CLR och du inte är på SQL Server 2012 eller högre än).

Markör

Alla får höra att markörer är onda och att de bör undvikas till varje pris, men detta slår faktiskt prestandan för de flesta andra stödda metoder och är säkrare än den udda uppdateringen. De enda jag föredrar framför markörlösningen är 2012 och CLR-metoderna (nedan):

CREATE TABLE #x
(
  TID INT PRIMARY KEY, 
  amt INT, 
  rt INT
);

INSERT #x(TID, amt) 
  SELECT TID, amt
  FROM dbo.Transactions
  ORDER BY TID;

DECLARE @rt INT, @tid INT, @amt INT;
SET @rt = 0;

DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  FOR SELECT TID, amt FROM #x ORDER BY TID;

OPEN c;

FETCH c INTO @tid, @amt;

WHILE @@FETCH_STATUS = 0
BEGIN
  SET @rt = @rt + @amt;
  UPDATE #x SET rt = @rt WHERE TID = @tid;
  FETCH c INTO @tid, @amt;
END

CLOSE c; DEALLOCATE c;

SELECT TID, amt, RunningTotal = rt 
  FROM #x 
  ORDER BY TID;

DROP TABLE #x;

SQL Server 2012 eller senare

Nya fönsterfunktioner introducerade i SQL Server 2012 gör den här uppgiften mycket enklare (och den fungerar bättre än alla ovanstående metoder också):

SELECT TID, amt, 
  RunningTotal = SUM(amt) OVER (ORDER BY TID ROWS UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;

Observera att på större datamängder kommer du att upptäcka att ovanstående presterar mycket bättre än något av följande två alternativ, eftersom RANGE använder en spool på disken (och standarden använder RANGE). Men det är också viktigt att notera att beteendet och resultaten kan skilja sig åt, så se till att de båda ger korrekta resultat innan du bestämmer mellan dem baserat på denna skillnad.

SELECT TID, amt, 
  RunningTotal = SUM(amt) OVER (ORDER BY TID)
FROM dbo.Transactions
ORDER BY TID;

SELECT TID, amt, 
  RunningTotal = SUM(amt) OVER (ORDER BY TID RANGE UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;

CLR

För fullständighetens skull erbjuder jag en länk till Pavel Pawlowskis CLR-metod, som är den överlägset att föredra på versioner före SQL Server 2012 (men inte 2000 uppenbarligen).

http://www.pawlowski.cz/2010/09/sql-server-and-fastest-running-totals-using-clr/

Slutsats

Om du använder SQL Server 2012 eller senare är valet självklart - använd den nya SUM() OVER() konstruktion (med ROWS kontra RANGE ). För tidigare versioner vill du jämföra prestandan för de alternativa tillvägagångssätten på ditt schema, data och - med icke-prestationsrelaterade faktorer i åtanke - avgöra vilket tillvägagångssätt som är rätt för dig. Det kan mycket väl vara CLR-metoden. Här är mina rekommendationer, i prioritetsordning:

  1. SUM() OVER() ... ROWS , om den är 2012 eller senare
  2. CLR-metod, om möjligt
  3. Första rekursiva CTE-metoden, om möjligt
  4. Markör
  5. De andra rekursiva CTE-metoderna
  6. Konstig uppdatering
  7. Gå med och/eller relaterad underfråga

För ytterligare information om prestandajämförelser av dessa metoder, se den här frågan på http://dba.stackexchange.com:

https://dba.stackexchange.com/questions/19507/running-total-with-count

Jag har också bloggat mer information om dessa jämförelser här:

http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals

Också för grupperade/partitionerade löpande summor, se följande inlägg:

http://sqlperformance.com/2014/01/t-sql-queries/grouped-running-totals

Partitionering resulterar i en löpande totalfråga

Flera löpande summor med Gruppera efter



  1. Hur du snabbar upp din SQL-server med hjälp av databasprestandaövervakning

  2. Skicka parameter till MySQL-skriptkommandoraden

  3. Vad är skillnaden mellan USER() och SYS_CONTEXT('USERENV','CURRENT_USER')?

  4. Ställa in MySQL root-användarlösenordet på OS X