sql >> Databasteknik >  >> RDS >> Database

Bästa tillvägagångssätt för grupperade löpande summor

Det allra första blogginlägget på den här webbplatsen, långt tillbaka i juli 2012, talade om de bästa metoderna för löpande totaler. Sedan dess har jag blivit tillfrågad vid flera tillfällen hur jag skulle ställa mig till problemet om de löpande summorna var mer komplexa – specifikt om jag behövde beräkna löpande summor för flera enheter – t.ex. varje kunds beställningar.

Det ursprungliga exemplet använde ett fiktivt fall av en stad som utfärdade fortkörningsböter; den löpande summan var helt enkelt att sammanställa och hålla en löpande räkning av antalet fortkörningsböter per dag (oavsett vem biljetten utfärdades till eller hur mycket den gällde). Ett mer komplext (men praktiskt) exempel kan vara att sammanställa det löpande totala värdet av fortkörningsböter, grupperade efter körkort, per dag. Låt oss föreställa oss följande tabell:

SKAPA TABELL dbo.SpeedingTickets( IncidentID INT IDENTITY(1,1) PRIMARY KEY, License Number INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL); SKAPA UNIKT INDEX x PÅ dbo.SpeedingTickets(LicenseNumber, IncidentDate) INCLUDE(TicketAmount);

Du kanske frågar, DECIMAL(7,2) , verkligen? Hur snabbt går dessa människor? Tja, i Kanada, till exempel, är det inte så svårt att få 10 000 USD fortkörningsböter.

Låt oss nu fylla i tabellen med några exempeldata. Jag kommer inte att gå in på alla detaljerna här, men det här bör producera cirka 6 000 rader som representerar flera förare och flera biljettbelopp under en månadslång period:

;WITH TicketAmounts(ID,Value) AS ( -- 10 godtyckliga biljettbelopp SELECT i,p FROM ( VALUES(1,32,75),(2,75), (3,109),(4,175),(5,295), (6,68.50),(7.125),(8.145),(9.199),(10.250) ) AS v(i,p)),LicenseNumbers(LicenseNumber,[new]) AS ( -- 1000 slumpmässiga licensnummer SELECT TOP ( 1000) 7000000 + nummer, n =NEWID() FRÅN [master].dbo.spt_values ​​WHERE nummer MELLAN 1 OCH 999999 BESTÄLLS AV n),JanuaryDates([dag]) AS ( -- varje dag i januari 2014 VÄLJ TOPP (31) DATEADD(DAY, number, '20140101') FRÅN [master].dbo.spt_values ​​WHERE [typ] =N'P' ORDER BY number),Tickets(LicenseNumber,[day],s) AS( -- matcha *några* licenser till dagar de fick biljetter SELECT DISTINCT l.LicenseNumber, d.[day], s =RTRIM(l.LicenseNumber) FROM LicenseNumbers AS l CROSS JOIN JanuaryDates AS d WHERE CHECKSUM(NEWID()) % 100 =l.LicenseNumber % AND (RTRIM(l.LicenseNumber) LIKE '%' + RIGHT(CONVERT(CHAR(8), d.[day], 112),1) + '%') OR (RTRIM(l.LicenseNumber+1) LIKE ' %' + HÖGER( CONVERT(CHAR(8), d.[dag], 112),1) + '%'))INSERT dbo.SpeedingTickets(LicenseNumber,IncidentDate,TicketAmount)SELECT t.LicenseNumber,t.[day], ta.Value FROM Biljetter AS t INNER JOIN TicketAmounts AS ta ON ta.ID =CONVERT(INT,RIGHT(t.s,1))-CONVERT(INT,LEFT(RIGHT(t.s,2),1)) BESTÄLL AV t.[dag], t .LicenseNumber;

Detta kan tyckas lite för involverat, men en av de största utmaningarna jag ofta har när jag skriver dessa blogginlägg är att konstruera en lämplig mängd realistisk "slumpmässig" / godtycklig data. Om du har en bättre metod för godtycklig datapopulation, använd för all del inte mina mumlande som exempel – de är perifera till det här inlägget.

Tillvägagångssätt

Det finns olika sätt att lösa detta problem i T-SQL. Här är sju tillvägagångssätt, tillsammans med deras tillhörande planer. Jag har utelämnat tekniker som markörer (eftersom de onekligen kommer att vara långsammare) och datumbaserade rekursiva CTE:er (eftersom de beror på sammanhängande dagar).

    Underfråga #1

    SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =TicketAmount + COALESCE( ( SELECT SUM(TicketAmount) FROM dbo.SpeedingTickets AS s WHERE s.LicenseNumber =o.LicenseNumber AND s.IncidentDate)  


    Planera för underfråga #1

    Underfråga #2

    SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =( SELECT SUM(TicketAmount) FROM dbo.SpeedingTickets WHERE LicenseNumber =t.LicenseNumber AND IncidentDate <=t.IncidentDate )FROM dbo.SpeedingTicketsBY LicenseNumber LicenseNumber AND IncidentDate <=t.IncidentDate )FRÅN dbo.SpeedingTicketsBY LicentseNumber AS;> 


    Planera för delfråga #2

    Gå med själv

    VÄLJ t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal =SUM(t2.TicketAmount)FRÅN dbo.SpeedingTickets AS t1INNER JOIN dbo.SpeedingTickets AS t2 ON t1.LicenseTumbericent.Indent. t2.IncidentDateGROUP BY t1.LicenseNumber, t1.IncidentDate, t1.TicketAmountORDER BY t1.LicenseNumber, t1.IncidentDate;


    Planera för självanslutning

    Yttertillämpning

    VÄLJ t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal =SUM(t2.TicketAmount)FRÅN dbo.SpeedingTickets SOM t1OUTRE APPLY( SELECT TicketAmount FROM dbo.SpeedingTickets Incensomer.Datumber LicentTickets.DatumLicent IncidentDate) SOM t2GROUP BY t1.LicenseNumber, t1.IncidentDate, t1.TicketAmountORDER BY t1.LicenseNumber, t1.IncidentDate;


    Planera för yttre ansökan

    SUMMA ÖVER() med RANGE (endast 2012+)

    SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate RANGE UNBOUNDED PRECEDING ) FROM dbo.SpeedingTickets ORDER BY LicenseNumber;
    IncidentDatum;


    Planera för SUM OVER() med RANGE

    SUMMA ÖVER() med ROWS (endast 2012+)

    SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate ROWS UNBOUNDED PRECEDING ) FROM dbo.SpeedingTickets BESTÄLLNING EFTER LicenseNumber,
    IncidentDatumber;


    Planera för SUM OVER() med ROWS

    Uppsättningsbaserad iteration

    Med kredit till Hugo Kornelis (@Hugo_Kornelis) för kapitel #4 i SQL Server MVP Deep Dives Volume #1, kombinerar detta tillvägagångssätt ett set-baserat tillvägagångssätt och en markörstrategi.

    DECLARE @x TABLE( LicensNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL, RunningTotal DECIMAL(7,2) NOT NULL, rn INT NOT NULL, PRIMARY KEY(LicensDatum), ); INSERT @x(LicenseNumber, IncidentDate, TicketAmount, RunningTotal, rn)SELECT LicenseNumber, IncidentDate, TicketAmount, TicketAmount, ROW_NUMBER() OVER (PARTITION BY LicenseNumber ORDER BY IncidentDate) FROM dbo.SpeedingTickets; DEKLARE @rn INT =1, @rc INT =1; WHILE @rc> 0BEGIN SET @rn +=1; UPPDATERA [current] SET RunningTotal =[senast].RunningTotal + [current].Biljettbelopp FRÅN @x AS [current] INNER JOIN @x AS [senast] PÅ [current].LicenseNumber =[senast].LicenseNumber AND [senast]. rn =@rn - 1 WHERE [nuvarande].rn =@rn; SET @rc =@@ROWCOUNT;END SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal FROM @x ORDER BY LicenseNumber, IncidentDate;

    På grund av sin natur producerar detta tillvägagångssätt många identiska planer i processen att uppdatera tabellvariabeln, som alla liknar planerna för självanslutning och yttre applicering, men som kan använda en sökning:


    En av många UPPDATERINGsplaner som producerats genom uppsättningsbaserad iteration

    Den enda skillnaden mellan varje plan i varje iteration är radantalet. Genom varje efterföljande iteration bör antalet påverkade rader förbli detsamma eller minska, eftersom antalet påverkade rader vid varje iteration representerar antalet förare med biljetter på det antalet dagar (eller, mer exakt, antalet dagar kl. den där "ranken").

Prestanda resultat

Här är hur tillvägagångssätten staplades upp, som visas av SQL Sentry Plan Explorer, med undantag för den uppsättningsbaserade iterationsmetoden som, eftersom den består av många individuella satser, inte representerar bra jämfört med resten.


Planera Explorer runtime-statistik för sex av de sju tillvägagångssätten

Förutom att granska planerna och jämföra körtidsmätningar i Plan Explorer, mätte jag även rå körtid i Management Studio. Här är resultaten av att köra varje fråga 10 gånger, tänk på att detta även inkluderar renderingstid i SSMS:


Körtid, i millisekunder, för alla sju tillvägagångssätt (10 iterationer) )

Så om du använder SQL Server 2012 eller bättre verkar det bästa tillvägagångssättet vara SUM OVER() med ROWS UNBOUNDED PRECEDING . Om du inte använder SQL Server 2012, verkade den andra subquery-metoden vara optimal när det gäller körtid, trots det höga antalet läsningar jämfört med t.ex. OUTER APPLY fråga. I alla fall bör du givetvis testa dessa tillvägagångssätt, anpassade till ditt schema, mot ditt eget system. Dina data, index och andra faktorer kan leda till att en annan lösning är mest optimal i din miljö.

Andra komplexitet

Nu betyder det unika indexet att en kombination av LicenseNumber + IncidentDate kommer att innehålla en enda kumulativ summa, i händelse av att en specifik förare får flera biljetter en viss dag. Denna affärsregel hjälper till att förenkla vår logik lite, och undviker behovet av en tie-breaker för att producera deterministiska löpande summor.

Om du har fall där du kan ha flera rader för en given kombination av LicenseNumber + IncidentDate, kan du bryta kopplingen med en annan kolumn som hjälper till att göra kombinationen unik (uppenbarligen skulle källtabellen inte längre ha en unik begränsning för dessa två kolumner) . Observera att detta är möjligt även i de fall där DATE kolumnen är faktiskt DATETIME – många antar att datum/tidsvärden är unika, men det är definitivt inte alltid garanterat, oavsett granularitet.

I mitt fall skulle jag kunna använda IDENTITY kolumn, IncidentID; här är hur jag skulle justera varje lösning (som erkänner att det kan finnas bättre sätt; bara kasta ut idéer):

/* --------- underfråga #1 ---------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =TicketAmount + COALESCE( ( SELECT SUM(TicketAmount) FROM dbo. SpeedingTickets AS s WHERE s.LicenseNumber =o.LicenseNumber AND (s.IncidentDate =t2.IncidentDate -- lade till den här raden:AND t1.IncidentID>=t2.IncidentIDNumber,LCencidentID1UP. .TicketAmountORDER BY t1.LicenseNumber, t1.IncidentDate; /* --------- yttre gäller --------- */ SELECT t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal =SUM(t2.TicketAmount)FROM dbo.SpeedingTickets AS t1OUTER APPLY( SELECT TicketAmount FROM dbo.SpeedingTickets WHERE LicenseNumber =t1.LicenseNumber AND IncidentDate <=t1.IncidentDate -- la till denna rad:AND IncidentID <=t1.IncidentID) AS t2GROUP BY t1NickAt.Incident.Incident.Incident. AV t1.LicenseNumber, t1.IncidentDate; /* --------- SUM() OVER using RANGE --------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate, IncidentID-OMRÅDE OBEGRÄNSAT FÖREGÅENDE -- lade till den här kolumnen ^^^^^^^^^^^^ ) FRÅN dbo.SpeedingTickets BESTÄLLNING EFTER LicenseNumber, IncidentDate; /* --------- SUM() OVER using ROWS --------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate, IncidentID RADER OBEGRÄNSAD FÖREGÅENDE -- lade till den här kolumnen ^^^^^^^^^^^^ ) FRÅN dbo.SpeedingTickets BESTÄLLNING EFTER LicenseNumber, IncidentDate; /* ---------- uppsättningsbaserad iteration ---------- */ DECLARE @x TABLE( -- lade till denna kolumn och gjorde den till PK:IncidentID INT PRIMARY KEY, LicenseNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL, RunningTotal DECIMAL(7,2) NOT NULL, rn INT NOT NULL); -- lade till den extra kolumnen i INSERT/SELECT:INSERT @x(IncidentID, LicenseNumber, IncidentDate, TicketAmount, RunningTotal, rn)SELECT IncidentID, LicenseNumber, IncidentDate, TicketAmount, TicketAmount, ROW_NUMBER() OVER (PARTITION BY IncidentORDateNumber , IncidentID) -- och la till denna tie-breaker-kolumn -----------------------------^^^^^^^^ ^^^^ FRÅN dbo.SpeedingTickets; -- resten av den uppsättningsbaserade iterationslösningen förblev oförändrad

En annan komplikation du kan stöta på är när du inte är ute efter hela tabellen, utan snarare en delmängd (säg, i det här fallet, första veckan i januari). Du måste göra justeringar genom att lägga till WHERE satser, och håll dessa predikaten i åtanke när du också har korrelerade underfrågor.


  1. Hur man distribuerar MariaDB Cluster 10.5 för hög tillgänglighet

  2. En känslolös logisk titt på SQL Servers namnkonventioner

  3. Fråga efter data genom att sammanfoga två tabeller i två databaser på olika servrar

  4. Vad är Microsoft Access? En kort introduktion för nya användare