sql >> Database teknologi >  >> RDS >> Sqlserver

Beregn løbende total / løbende balance

For dem, der ikke bruger SQL Server 2012 eller nyere, er en markør sandsynligvis den mest effektive understøttede og garanteret metode uden for CLR. Der er andre tilgange såsom den "quirky opdatering", som kan være marginalt hurtigere, men ikke garanteret vil virke i fremtiden, og selvfølgelig sæt-baserede tilgange med hyperbolske præstationsprofiler, efterhånden som tabellen bliver større, og rekursive CTE-metoder, der ofte kræver direkte #tempdb I/O eller resultere i spild, der giver nogenlunde samme effekt.

INNER JOIN - gør ikke dette:

Den langsomme, sæt-baserede tilgang har 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;
 

Grunden til at dette er langsomt? Efterhånden som tabellen bliver større, kræver hver trinvis række læsning af n-1 rækker i tabellen. Dette er eksponentielt og bundet til fejl, timeouts eller bare vrede brugere.

Korreleret underforespørgsel - gør heller ikke dette:

Underforespørgselsformularen er på samme måde smertefuld af lignende smertefulde årsager.

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;
 

Quirky opdatering - gør dette på egen risiko:

Den "quirky update" metode er mere effektiv end ovenstående, men adfærden er ikke dokumenteret, der er ingen garantier for orden, og adfærden fungerer muligvis i dag, men kan gå i stykker i fremtiden. Jeg medtager dette, fordi det er en populær metode, og det er effektivt, men det betyder ikke, at jeg støtter det. Den primære grund til, at jeg selv besvarede dette spørgsmål i stedet for at lukke det som en dublet, er, fordi det andet spørgsmål har en finurlig opdatering som det accepterede 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;
 

Rekursive CTE'er

Denne første er afhængig af, at TID er sammenhængende, ingen huller:

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

Hvis du ikke kan stole på dette, kan du bruge denne variant, som blot bygger en sammenhængende sekvens ved hjælp af 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);
 

Afhængigt af størrelsen af ​​dataene (f.eks. kolonner, vi ikke kender til), kan du muligvis finde bedre generel ydeevne ved kun at fylde de relevante kolonner i en #temp-tabel først og behandle mod den i stedet for basistabellen:

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;
 

Kun den første CTE-metode vil levere ydeevne, der konkurrerer med den finurlige opdatering, men den gør en stor antagelse om dataenes art (ingen huller). De to andre metoder vil falde tilbage, og i de tilfælde kan du lige så godt bruge en markør (hvis du ikke kan bruge CLR, og du endnu ikke er på SQL Server 2012 eller nyere).

Markør

Alle får at vide, at markører er onde, og at de bør undgås for enhver pris, men dette slår faktisk ydeevnen for de fleste andre understøttede metoder og er sikrere end den skæve opdatering. De eneste, jeg foretrækker frem for markørløsningen, er 2012- og CLR-metoderne (nedenfor):

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 nyere

Nye vinduesfunktioner introduceret i SQL Server 2012 gør denne opgave meget nemmere (og den yder også bedre end alle ovenstående metoder):

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

Bemærk, at på større datasæt vil du opdage, at ovenstående yder meget bedre end nogen af ​​de følgende to muligheder, da RANGE bruger en spool på disken (og standarden bruger RANGE). Det er dog også vigtigt at bemærke, at adfærd og resultater kan variere, så sørg for, at de begge returnerer korrekte resultater, før du beslutter mellem dem baseret på denne forskel.

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

For fuldstændighedens skyld tilbyder jeg et link til Pavel Pawlowskis CLR-metode, som er langt den foretrukne metode på versioner før SQL Server 2012 (men naturligvis ikke 2000).

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

Konklusion

Hvis du er på SQL Server 2012 eller nyere, er valget oplagt - brug den nye SUM() OVER() konstruktion (med ROWS vs. RANGE ). For tidligere versioner vil du gerne sammenligne ydeevnen af ​​de alternative tilgange på dit skema, data og - tage ikke-ydelsesrelaterede faktorer i tankerne - bestemme, hvilken tilgang der er den rigtige for dig. Det kan meget vel være CLR-tilgangen. Her er mine anbefalinger i foretrukket rækkefølge:

  1. SUM() OVER() ... ROWS , hvis den er i 2012 eller derover
  2. CLR-metode, hvis det er muligt
  3. Første rekursive CTE-metode, hvis det er muligt
  4. Markør
  5. De andre rekursive CTE-metoder
  6. Quirky opdatering
  7. Deltag og/eller korreleret underforespørgsel

For yderligere information om præstationssammenligninger af disse metoder, se dette spørgsmål på http://dba.stackexchange.com:

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

Jeg har også blogget flere detaljer om disse sammenligninger her:

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

Også for grupperede/partitionerede løbende totaler, se følgende indlæg:

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

Partitionering resulterer i en kørende totalforespørgsel

Flere løbende totaler med Grupper efter



  1. Sådan vælger du sammenligning af to kolonner som én kolonne i Oracle

  2. Sådan opretter du en ikke-nul kolonne i en visning

  3. En praktisk brug af SQL COALESCE-funktionen

  4. Tilslutning til Oracle Database ved hjælp af SQL Server Integration Services