Database
 sql >> Baza danych >  >> RDS >> Database

Najlepsze podejścia do zgrupowanych sum biegowych

Pierwszy post na tej stronie, w lipcu 2012 roku, mówił o najlepszych metodach obliczania sum biegowych. Od tego czasu wielokrotnie pytano mnie, jak podszedłbym do problemu, gdyby sumy bieżące były bardziej złożone – w szczególności, gdybym musiał obliczyć sumy bieżące dla wielu podmiotów – powiedzmy, zamówienia każdego klienta.

W oryginalnym przykładzie wykorzystano fikcyjny przypadek miasta wystawiającego mandaty za przekroczenie prędkości; bieżąca suma była po prostu sumowaniem i prowadzeniem bieżącej liczby mandatów za przekroczenie prędkości w ciągu dnia (niezależnie od tego, komu bilet został wystawiony i za ile był). Bardziej złożonym (ale praktycznym) przykładem może być sumowanie dziennej łącznej wartości mandatów za przekroczenie prędkości, pogrupowanych według prawa jazdy. Wyobraźmy sobie następującą tabelę:

CREATE TABLE dbo.SpeedingTickets( IncidentID INT IDENTITY(1,1) PRIMARY KEY, Numer licencji INT NOT NULL, IncidentDate DATA NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL); UTWÓRZ UNIKALNY INDEKS x NA dbo.SpeedingTickets(LicenseNumber, IncidentDate) INCLUDE(TicketAmount);

Możesz zapytać, DECIMAL(7,2) , naprawdę? Jak szybko ci ludzie idą? Cóż, na przykład w Kanadzie nie jest aż tak trudno uzyskać mandat w wysokości 10 000 USD za przekroczenie prędkości.

Teraz wypełnijmy tabelę przykładowymi danymi. Nie będę tutaj omawiał wszystkich szczegółów, ale powinno to dać około 6000 wierszy reprezentujących wielu kierowców i wiele kwot biletów w ciągu miesiąca:

;WITH TicketAmounts(ID,Value) AS ( -- 10 dowolnych kwot biletów SELECT i,p FROM ( VALUES(1,32,75),(2,75),(3109),(4,175),(5295), ( 1000) 7000000 + liczba, n =NEWID() FROM [master].dbo.spt_values ​​WHERE number BETWEEN 1 AND 999999 ORDER BY n),JanuaryDates([day]) AS ( -- codziennie w styczniu 2014 SELECT TOP (31) DATEADD(DZIEŃ, liczba, '20140101') FROM [master].dbo.spt_values ​​WHERE [typ] =N'P' ORDER BY numer),Tickets(NumerLicencji,[dzień],s) AS( -- dopasowanie *jakaś* licencje do dni, w których otrzymali bilety SELECT DISTINCT l.NumerLicencji, d.[dzień], s =RTRIM(l.NumerLicencji) FROM NumeryLicencji AS l CROSS JOIN StyczeńDaty AS d WHERE SUMA KONTROLNA(NEWID()) % 100 =l.NumerLicencji % 100 AND (RTRIM(l.LicenseNumber) LIKE '%' + RIGHT(CONVERT(CHAR(8), d.[day], 112),1) + '%') OR (RTRIM(l.LicenseNumber+1) LIKE ' %' + PRAWO( CONVERT(CHAR(8), d.[dzień], 112),1) + '%'))INSERT dbo.SpeedingTickets(NumerLicencji,DataIncydentu,KwotaBiletu)SELECT t.NumerLicencji, t.[dzień], ta.Wartość FROM Tickety AS t INNER JOIN Kwoty Biletów AS ta ON ta.ID =CONVERT(INT,RIGHT(t.s,1))-CONVERT(INT,LEFT(RIGHT(t.s,2),1)) ORDER BY t.[day], t .Numer licencji;

To może wydawać się trochę zbyt skomplikowane, ale jednym z największych wyzwań, jakie często napotykam podczas pisania tych postów na blogu, jest skonstruowanie odpowiedniej ilości realistycznych „losowych” / arbitralnych danych. Jeśli masz lepszą metodę na arbitralne gromadzenie danych, za wszelką cenę nie używaj moich mamrotań jako przykładu – są one marginalne w stosunku do tego postu.

Podejścia

Istnieje wiele sposobów rozwiązania tego problemu w T-SQL. Oto siedem podejść wraz z powiązanymi planami. Pominąłem techniki, takie jak kursory (ponieważ będą niezaprzeczalnie wolniejsze) i rekurencyjne CTE oparte na dacie (ponieważ zależą od ciągłych dni).

    Podzapytanie nr 1

    SELECT Numer Licencji, Data Incydentu, Kwota Biletu, Razem Uruchomione =Kwota Biletu + COALESCE ( ( SELECT SUM (Kwota Biletu) FROM dbo. SpeedingTickets AS s WHERE s. Numer licencji =o. Numer licencji AND s. Data Incydentu  


    Plan dla podzapytania nr 1

    Podzapytanie nr 2

    SELECT NumerLicencji, DataIncydentu, KwotaBiletu, Suma Bieżącego =( SELECT SUM(KwotaBiletu) FROM dbo.SpeedingTickets WHERE NumerLicencji =t.NumerLicencji AND DataIncydentu <=t.DataIncydentu )FROM dbo.SpeedingTickets AS tORDER BY Numer licencji; 


    Plan dla podzapytania nr 2

    Samozłączenie

    SELECT t1.Numer licencji, t1.IncydentDate, t1.Kwota biletu, RunningTotal =SUM(t2.Kwota biletu)FROM dbo.SpeedingTickets AS t1INNER JOIN dbo.SpeedingTickets AS t2 ON t1.LicenseNicate.NumberId2. t2.IncidentDateGROUP BY t1.LicenseNumber, t1.IncidentDate, t1.TicketAmountORDER BY t1.LicenseNumber, t1.IncidentDate;


    Plan samodzielnego dołączania

    Zewnętrzne zastosowanie

    SELECT t1.Numer licencji, t1.IncydentDate, t1.Kwota biletu, RunningTotal =SUM(t2.Kwota biletu)FROM dbo.SpeedingTickets AS t1OUTER APPLY( SELECT TicketAmount FROM dbo.SpeedingTickets WHERE Numer licencji1.Numer t1. IncidentDate) AS t2GROUP BY t1.LicenseNumber, t1.IncidentDate, t1.TicketAmountORDER BY t1.LicenseNumber, t1.IncidentDate;


    Plan dla zastosowania zewnętrznego

    SUM OVER() przy użyciu RANGE (tylko 2012+)

    SELECT NumerLicencji, DataIncydentu, KwotaBiletu, Suma_Rzecz. =SUM(Kwota Biletu) OVER (PARTYCJA BY Numer licencji ORDER BY IncidentDate ZAKRES BEZ OGRANICZEŃ PRECEDING ) FROM dbo.SpeedingTickets ORDER BY Numer licencji, IncidentDate;


    Zaplanuj SUM OVER() przy użyciu RANGE

    SUM OVER() przy użyciu ROWS (tylko 2012+)

    SELECT NumerLicencji, DataIncydentu, KwotaBiletu, Suma_Rzecz. =SUM(KwotaBiletu) OVER (PARTYCJA BY Numer licencji ORDER BY IncidentDate ROWS UNBOUNDED PRECEDING ) FROM dbo.SpeedingTickets ORDER BY Numer licencji, IncidentDate;


    Zaplanuj SUM OVER() przy użyciu ROWS

    Iteracja oparta na zestawach

    Z uznaniem dla Hugo Kornelisa (@Hugo_Kornelis) za rozdział 4 w SQL Server MVP Deep Dives Volume #1, to podejście łączy podejście oparte na zbiorach i podejście oparte na kursorze.

    DECLARE @x TABLE( Numer licencji INT NOT NULL, IncidentDate DATA NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL, RunningTotal DECIMAL(7,2) NOT NULL, rn INT NOT NULL, PRIMARY KEY(LicenseNumber, IncidentDate) ); INSERT @x(LicenseNumber, IncidentDate, TicketAmount, RunningTotal, rn)SELECT SELECT LicenseNumber, IncidentDate, TicketAmount, TicketAmount, ROW_NUMBER() OVER (PARTITION BY LicenseNumber ORDER BY IncidentDate) FROM dbo.SpeedingTickets; ZADEKLARUJ @rn INT =1, @rc INT =1; WHILE @rc> 0BEGIN SET @rn +=1; UPDATE [bieżący] SET RunningTotal =[ostatni].RunningTotal + [bieżący].TicketAmount FROM @x AS [bieżący] INNER JOIN @x AS [ostatni] ON [bieżący].Numer licencji =[ostatni].Numer licencji AND [ostatni]. rn =@rn - 1 GDZIE [bieżący].rn =@rn; SET @rc =@@ROWCOUNT;END SELECT Numer licencji, data incydentu, kwota biletu, suma uruchomienia FROM @x ORDER BY Numer licencji, data incydentu;

    Ze względu na swój charakter, to podejście generuje wiele identycznych planów w procesie aktualizacji zmiennej tabeli, z których wszystkie są podobne do planów samodzielnego łączenia i zewnętrznego zastosowania, ale mogą korzystać z wyszukiwania:


    Jeden z wielu planów UPDATE utworzonych za pomocą iteracji opartej na zestawie

    Jedyną różnicą między każdym planem w każdej iteracji jest liczba wierszy. W każdej kolejnej iteracji liczba wierszy, których to dotyczy, powinna pozostać taka sama lub maleć, ponieważ liczba wierszy, których dotyczy każda iteracja, reprezentuje liczbę kierowców posiadających bilety na tę liczbę dni (a dokładniej liczbę dni w ta "ranga").

Wyniki wydajności

Oto jak ułożyły się podejścia, jak pokazuje SQL Sentry Plan Explorer, z wyjątkiem podejścia opartego na zbiorach, które, ponieważ składa się z wielu indywidualnych instrukcji, nie reprezentuje się dobrze w porównaniu z resztą.


Wskaźniki czasu działania programu Plan Explorer dla sześciu z siedmiu podejść

Oprócz przeglądania planów i porównywania metryk środowiska wykonawczego w Eksploratorze planów, mierzyłem również surowe środowisko wykonawcze w Management Studio. Oto wyniki 10-krotnego uruchomienia każdego zapytania, pamiętając, że obejmuje to również czas renderowania w SSMS:


Czas działania w milisekundach dla wszystkich siedmiu podejść (10 iteracji )

Tak więc, jeśli korzystasz z SQL Server 2012 lub nowszego, najlepszym podejściem wydaje się być SUM OVER() używając ROWS UNBOUNDED PRECEDING . Jeśli nie korzystasz z SQL Server 2012, drugie podejście podzapytania wydaje się optymalne pod względem czasu wykonywania, pomimo dużej liczby odczytów w porównaniu z, powiedzmy, OUTER APPLY zapytanie. We wszystkich przypadkach, oczywiście, powinieneś przetestować te podejścia, dostosowane do twojego schematu, z własnym systemem. Twoje dane, indeksy i inne czynniki mogą prowadzić do tego, że inne rozwiązanie będzie najbardziej optymalne w Twoim środowisku.

Inne zawiłości

Teraz unikalny indeks oznacza, że ​​dowolna kombinacja Numer Licencji + Data Zdarzenia będzie zawierać jedną łączną sumę w przypadku, gdy określony kierowca otrzyma wiele biletów w danym dniu. Ta reguła biznesowa pomaga nieco uprościć naszą logikę, unikając potrzeby rozstrzygania remisów w celu uzyskania deterministycznych sum bieżących.

Jeśli masz przypadki, w których możesz mieć wiele wierszy dla dowolnej kombinacji Numer Licencji + Data Incydentu, możesz przerwać remis, używając innej kolumny, która pomaga uczynić tę kombinację unikalną (oczywiście tabela źródłowa nie będzie już miała ograniczenia unikatowego dla tych dwóch kolumn) . Pamiętaj, że jest to możliwe nawet w przypadkach, gdy DATE kolumna to w rzeczywistości DATETIME – wiele osób zakłada, że ​​wartości daty/czasu są unikalne, ale z pewnością nie zawsze jest to gwarantowane, niezależnie od szczegółowości.

W moim przypadku mógłbym użyć IDENTITY kolumna, IncidentID; oto jak dostosowałbym każde rozwiązanie (przyznając, że mogą istnieć lepsze sposoby; po prostu wyrzucając pomysły):

/* --------- podzapytanie #1 --------- */ SELECT Numer licencji, Data zdarzenia, Kwota_biletu, Suma_ruchu =Kwota_biletu + COALESCE( ( SELECT SUM(Kwota_biletu) FROM dbo. SpeedingTickets AS s WHERE s.LicenseNumber =o.LicenseNumber AND (s.IncidentDate =t2.IncidentDate -- dodano ten wiersz:AND t1.IncidentID>=t2.IncidentIDGROUP BY t1.LicenseNumber .Kwota biletuZAMÓW PRZEZ t1.Numer licencji, t1.IncidentDate; /* --------- zewnętrzna aplikacja --------- */ 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 -- dodano ten wiersz:AND IncidentID <=t1.IncidentID) AS t2GROUP BY t1.LicenseNumber.Incident1.Dt1. BY t1.LicenseNumber, t1.IncidentDate; /* --------- SUM() OVER przy użyciu RANGE --------- */ SELECT NumerLicencji, DataIncydentu, KwotaBiletu, Suma_Ruchu =SUM(KwotaBiletu) OVER ( PARTITION BY NumerLicencji ORDER BY DataIncydentu, IncidentID ZAKRES BEZ OGRANICZEŃ PRECEDING -- dodano tę kolumnę ^^^^^^^^^^^^ ) FROM dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate; /* --------- SUM() OVER przy użyciu ROWS --------- */ SELECT NumerLicencji, DataIncydentu, KwotaBiletu, Suma_Ruchu =SUM(KwotaBiletu) OVER ( PARTITION BY NumerLicencji ORDER BY DataIncydentu, IncidentID ROWS UNBOUNDED PRECEDING -- dodano tę kolumnę ^^^^^^^^^^^^ ) FROM dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate; /* --------- iteracja oparta na zestawie --------- */ DECLARE @x TABLE( -- dodał tę kolumnę i uczynił ją PK:IncidentID INT PRIMARY KEY, LicenseNumber INT NOT NULL, IncidentDate DATA NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL, RunningTotal DECIMAL(7,2) NOT NULL, rn INT NOT NULL); -- dodano dodatkową kolumnę do INSERT/SELECT:INSERT @x(IncidentID, LicenseNumber, IncidentDate, TicketAmount, RunningTotal, rn)SELECT IncidentID, LicenseNumber, IncidentDate, TicketAmount, TicketAmount, ROW_NUMBER() OVER (PARTITION BY LicenseNumber ORDER BY IncidentDate , IncidentID) -- i dodałem tę kolumnę rozstrzygającą ------------------------------^^^^^^^^ ^^^^ Z dbo.SpeedingTickets; -- reszta rozwiązania iteracyjnego opartego na zbiorach pozostała niezmieniona

Inną komplikacją, na którą możesz się natknąć, jest to, że nie szukasz całego stołu, ale raczej podgrupy (powiedzmy, w tym przypadku, pierwszy tydzień stycznia). Musisz wprowadzić poprawki, dodając WHERE klauzul i pamiętaj o tych predykatach, gdy masz skorelowane podzapytania.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Model danych dotyczących opieki nad zwierzętami

  2. Python, Ruby i Golang:porównanie aplikacji usług internetowych

  3. Jak sprawdzić, czy T-SQL UDF jest powiązany ze schematem (nawet jeśli jest zaszyfrowany)

  4. Co się właściwie dzieje z tym Seek?

  5. Predykat ma znaczenie w rozszerzonych zdarzeniach