sql >> Database >  >> RDS >> Sqlserver

Bereken lopend totaal / lopend saldo

Voor degenen die geen SQL Server 2012 of hoger gebruiken, is een cursor waarschijnlijk de meest efficiënte ondersteunde en gegarandeerd methode buiten CLR. Er zijn andere benaderingen, zoals de "eigenzinnige update", die iets sneller kan zijn maar niet gegarandeerd werkt in de toekomst, en natuurlijk set-gebaseerde benaderingen met hyperbolische prestatieprofielen naarmate de tabel groter wordt, en recursieve CTE-methoden die vaak directe #tempdb I/O of resulteren in lekkages die ongeveer dezelfde impact hebben.

INNER JOIN - doe dit niet:

De langzame, set-gebaseerde aanpak is van de vorm:

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;

De reden dat dit traag is? Naarmate de tabel groter wordt, moet voor elke incrementele rij n-1 rijen in de tabel worden gelezen. Dit is exponentieel en gebonden aan fouten, time-outs of gewoon boze gebruikers.

Gecorreleerde subquery - doe dit ook niet:

Het subquery-formulier is even pijnlijk om even pijnlijke redenen.

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;

Eigenzinnige update - doe dit op eigen risico:

De "eigenzinnige update"-methode is efficiënter dan de bovenstaande, maar het gedrag is niet gedocumenteerd, er zijn geen garanties over de bestelling en het gedrag zou vandaag kunnen werken, maar in de toekomst kunnen breken. Ik neem dit op omdat het een populaire methode is en het is efficiënt, maar dat betekent niet dat ik het onderschrijf. De belangrijkste reden dat ik deze vraag zelfs heb beantwoord in plaats van hem als een duplicaat te sluiten, is omdat de andere vraag een eigenaardige update heeft als het geaccepteerde antwoord.

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;

Recursieve CTE's

Deze eerste vertrouwt erop dat TID aaneengesloten is, geen gaten:

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

Als u hier niet op kunt vertrouwen, kunt u deze variant gebruiken, die eenvoudig een aaneengesloten reeks bouwt met behulp van 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);

Afhankelijk van de grootte van de gegevens (bijv. kolommen waarvan we niets weten), kunt u betere algehele prestaties vinden door de relevante kolommen eerst alleen in een #temp-tabel te vullen en daartegen te verwerken in plaats van de basistabel:

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;

Alleen de eerste CTE-methode zal prestaties leveren die wedijveren met de eigenzinnige update, maar het maakt een grote veronderstelling over de aard van de gegevens (geen gaten). De andere twee methoden vallen terug en in die gevallen kun je net zo goed een cursor gebruiken (als je CLR niet kunt gebruiken en je bent nog niet op SQL Server 2012 of hoger).

Cursor

Iedereen wordt verteld dat cursors slecht zijn en dat ze koste wat kost moeten worden vermeden, maar dit verslaat eigenlijk de prestaties van de meeste andere ondersteunde methoden en is veiliger dan de eigenzinnige update. De enige die ik verkies boven de cursoroplossing zijn de 2012- en CLR-methoden (hieronder):

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 of hoger

Nieuwe vensterfuncties geïntroduceerd in SQL Server 2012 maken deze taak een stuk eenvoudiger (en het presteert ook beter dan alle bovenstaande methoden):

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

Merk op dat u bij grotere datasets zult merken dat het bovenstaande veel beter presteert dan een van de volgende twee opties, aangezien RANGE een on-disk spool gebruikt (en de standaard RANGE gebruikt). Het is echter ook belangrijk op te merken dat het gedrag en de resultaten kunnen verschillen, dus zorg ervoor dat beide de juiste resultaten opleveren voordat u op basis van dit verschil een beslissing neemt.

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

Voor de volledigheid bied ik een link aan naar de CLR-methode van Pavel Pawlowski, die verreweg de voorkeur heeft voor versies vóór SQL Server 2012 (maar uiteraard niet 2000).

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

Conclusie

Als u SQL Server 2012 of hoger gebruikt, ligt de keuze voor de hand - gebruik de nieuwe SUM() OVER() constructie (met ROWS vs. RANGE ). Voor eerdere versies wilt u de prestaties van de alternatieve benaderingen voor uw schema en gegevens vergelijken en - rekening houdend met niet-prestatiegerelateerde factoren - bepalen welke benadering voor u geschikt is. Het zou heel goed de CLR-benadering kunnen zijn. Dit zijn mijn aanbevelingen, in volgorde van voorkeur:

  1. SUM() OVER() ... ROWS , indien in 2012 of hoger
  2. CLR-methode, indien mogelijk
  3. Eerste recursieve CTE-methode, indien mogelijk
  4. Cursor
  5. De andere recursieve CTE-methoden
  6. Eigenzinnige update
  7. Deelnemen aan en/of gecorreleerde subquery

Zie deze vraag op http://dba.stackexchange.com voor meer informatie over prestatievergelijkingen van deze methoden:

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

Ik heb hier ook meer details over deze vergelijkingen geblogd:

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

Zie ook de volgende berichten voor gegroepeerde/gepartitioneerde lopende totalen:

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

Partitionering resulteert in een lopende totalenquery

Meerdere lopende totalen met groeperen op



  1. Vind een waarde overal in een database

  2. Topfouten die moeten worden vermeden bij MySQL-replicatie

  3. Oracle .Net ManagedDataAccess-fout:kan het type 'OracleInternal.Common.ConfigBaseClass' niet laden vanuit de assembly

  4. FOUT:kon bibliotheek "/opt/PostgreSQL/9.0/lib/postgresql/plperl.so" niet laden:libperl.so: