sql >> Databáze >  >> RDS >> Database

Nejlepší přístupy pro seskupené průběžné součty

Úplně první blogový příspěvek na tomto webu z července 2012 hovořil o nejlepších přístupech k měření součtů. Od té doby jsem byl několikrát dotázán, jak bych k problému přistupoval, kdyby průběžné součty byly složitější – konkrétně kdybych potřeboval vypočítat průběžné součty pro více subjektů – řekněme objednávky každého zákazníka.

Původní příklad používal fiktivní případ města vydávajícího pokuty za překročení rychlosti; průběžný součet byl jednoduše agregováním a udržováním průběžného počtu pokut za překročení rychlosti za den (bez ohledu na to, komu byla jízdenka vydána nebo za jakou částku). Složitějším (ale praktickým) příkladem může být agregace průběžné celkové hodnoty pokut za překročení rychlosti seskupené podle řidičského průkazu za den. Představme si následující tabulku:

CREATE TABLE dbo.SpeedingTickets( IncidentID INT IDENTITY(1,1) PRIMÁRNÍ KLÍČ, LicenseNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL); CREATE UNIQUE INDEX x ON dbo.SpeedingTickets(LicenseNumber, IncidentDate) INCLUDE(TicketAmount);

Můžete se zeptat, DECIMAL(7,2) , opravdu? Jak rychle tito lidé jedou? No, například v Kanadě není tak těžké dostat pokutu za překročení rychlosti 10 000 $.

Nyní naplňte tabulku několika ukázkovými daty. Nebudu se zde rozepisovat o všech podrobnostech, ale mělo by to přinést asi 6 000 řádků představujících více řidičů a vícenásobné částky jízdenek za měsíční období:

;WITH TicketAmounts(ID,Value) AS ( -- 10 libovolných částek tiketu 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,[newid]) AS ( -- 1000 náhodných licenčních čísel SELECT TOP ( 1000) 7000000 + číslo, n =NEWID() FROM [master].dbo.spt_values ​​WHERE number BETWEEN 1 AND 999999 ORDER BY n),JanuaryDates([day]) AS ( -- každý den v lednu 2014 SELECT TOP (31) DATEADD(DAY, number, '20140101') FROM [master].dbo.spt_values ​​WHERE [type] =N'P' ORDER BY number),Tickets(LicenseNumber,[day],s) AS( -- match *some* licence do dnů, kdy dostali vstupenky VYBERTE DISTINCT l.LicenseNumber, d.[den], s =RTRIM(l.LicenseNumber) FROM LicenseNumbers AS l CROSS JOIN LedenData AS d WHERE CHECKSUUM(NEWID()) % 100 =l.LicenseNumber % AND (RTRIM(l.LicenseNumber) LIKE '%' + RIGHT(CONVERT(CHAR(8), d.[den], 112),1) + '%') NEBO (RTRIM(l.LicenseNumber+1) LIKE ' %' + DOPRAVA( CONVERT(CHAR(8), d.[den], 112),1) + '%'))INSERT dbo.SpeedingTickets(LicenseNumber,IncidentDate,TicketAmount)SELECT t.LicenseNumber, t[day], ta.Value FROM Vstupenky JAKO t VNITŘNÍ PŘIPOJENÍ Částky vstupenek AS ta ON ta.ID =CONVERT(INT,RIGHT(t.s,1))-CONVERT(INT,LEFT(RIGHT(t.s,2),1)) OBJEDNAT DO t.[den], t .LicenseNumber;

Může se to zdát příliš složité, ale jednou z největších výzev, se kterou se často setkávám při psaní těchto blogových příspěvků, je sestavení vhodného množství realistických „náhodných“ / libovolných dat. Pokud máte lepší metodu pro libovolnou populaci dat, rozhodně nepoužívejte moje mumlání jako příklad – jsou okrajové ve smyslu tohoto příspěvku.

Přístupy

Tento problém lze v T-SQL vyřešit různými způsoby. Zde je sedm přístupů spolu s jejich souvisejícími plány. Vynechal jsem techniky jako kurzory (protože budou nepopiratelně pomalejší) a rekurzivní CTE založené na datu (protože závisí na souvislých dnech).

    Poddotaz #1

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


    Plán pro dílčí dotaz č. 1

    Poddotaz #2

    SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =( SELECT SUM(TicketAmount) FROM dbo.SpeedingTickets WHERE LicenseNumber =t.LicenseNumber AND IncidentDate <=t.IncidentDate )OD dbo.SpeedtateTickets 


    Plán pro dílčí dotaz č. 2

    Vlastní připojení

    SELECT t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal =SUM(t2.TicketAmount)FROM dbo.SpeedingTickets AS t1INNER JOIN dbo.SpeedingTickets AS t2 ON t1.Lic.Lic.Lic. t2.IncidentDateGROUP BY t1.LicenseNumber, t1.IncidentDate, t1.TicketAmountORDER BY t1.LicenseNumber, t1.IncidentDate;


    Plán pro vlastní připojení

    Vnější použití

    SELECT t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal =SUM(t2.TicketAmount)FROM dbo.SpeedingTickets AS t1OUTER APPLY( SELECT TicketAmount=FROM dbo.SpeedingNumber t1.SpeedingTickets ANDWHEREDate. WHEREDate  


    Plán pro vnější použití

    SUM OVER() pomocí RANGE (pouze 2012+)

    SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER (ODDĚLENÍ PODLE LicenseNumber ORDER BY IncidentDate RANGE PŘEDCHOZÍ ROZSAH) OD dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate;


    Plánujte SUM OVER() pomocí RANGE

    SUM OVER() pomocí ŘÁDKŮ (pouze 2012+)

    SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER (ODDĚLENÍ PODLE LicenseNumber ORDER BY IncidentDate ŘÁDKY NEVZATÉ PŘEDCHOZÍ ) OD dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate;


    Plánujte SUM OVER() pomocí ŘÁDKŮ

    Iterace na základě sady

    Díky uznání Hugo Kornelis (@Hugo_Kornelis) za kapitolu #4 v SQL Server MVP Deep Dives Volume #1 tento přístup kombinuje přístup založený na množinách a přístup kurzoru.

    DECLARE @x TABLE( LicenseNumber 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(LicenseNumber, IncidentDate) ); INSERT @x(LicenseNumber, IncidentDate, TicketAmount, RunningTotal, rn)SELECT LicenseNumber, IncidentDate, TicketAmount, TicketAmount, ROW_NUMBER() OVER (ODDĚLENÍ PODLE LicenseNumber ORDER BY IncidentDate) OD dbo.SpeedingTickets; DECLARE @rn INT =1, @rc INT =1; WHILE @rc> 0BEGIN SET @rn +=1; AKTUALIZOVAT [aktuální] SET RunningTotal =[poslední].RunningTotal + [aktuální].TicketAmount OD @x JAKO [aktuální] VNITŘNÍ PŘIPOJENÍ @x JAKO [poslední] ZAPNUTO [aktuální].LicenseNumber =[poslední].LicenseNumber A [poslední]. rn =@rn - 1 WHERE [aktuální].rn =@rn; SET @rc =@@ROWCOUNT; END SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal FROM @x ORDER BY LicenseNumber, IncidentDate;

    Vzhledem ke své povaze tento přístup vytváří mnoho identických plánů v procesu aktualizace proměnné tabulky, z nichž všechny jsou podobné plánům self-join a externím aplikačním plánům, ale jsou schopny používat vyhledávací:


    Jeden z mnoha plánů UPDATE vytvořených iterací na základě sady

    Jediný rozdíl mezi každým plánem v každé iteraci je počet řádků. Během každé následné iterace by počet ovlivněných řádků měl zůstat stejný nebo klesnout, protože počet ovlivněných řádků při každé iteraci představuje počet řidičů s jízdenkami v daném počtu dní (nebo přesněji počet dní v že "hodnost").

Výsledky výkonu

Zde je ukázka toho, jak jsou přístupy naskládány, jak ukazuje SQL Sentry Plan Explorer, s výjimkou iteračního přístupu založeného na množinách, který, protože se skládá z mnoha jednotlivých příkazů, nereprezentuje dobře ve srovnání s ostatními.


Běhové metriky Plan Exploreru pro šest ze sedmi přístupů

Kromě kontroly plánů a porovnávání metrik běhu v Průzkumníkovi plánů jsem také změřil nezpracovaný běh v Management Studio. Zde jsou výsledky desetinásobného spuštění každého dotazu, přičemž je třeba mít na paměti, že to zahrnuje i dobu vykreslování v SSMS:


Doba běhu, v milisekundách, pro všech sedm přístupů (10 iterací )

Pokud tedy používáte SQL Server 2012 nebo lepší, zdá se, že nejlepší přístup je SUM OVER() pomocí ROWS UNBOUNDED PRECEDING . Pokud nepoužíváte SQL Server 2012, druhý přístup k poddotazu se z hlediska běhu zdál být optimální, a to i přes vysoký počet čtení v porovnání řekněme s OUTER APPLY dotaz. Ve všech případech byste samozřejmě měli otestovat tyto přístupy, přizpůsobené vašemu schématu, proti vašemu vlastnímu systému. Vaše data, indexy a další faktory mohou vést k tomu, že jiné řešení bude ve vašem prostředí nejoptimálnější.

Další složitosti

Jedinečný index nyní znamená, že jakákoli kombinace LicenseNumber + IncidentDate bude obsahovat jeden kumulativní součet v případě, že konkrétní řidič dostane více jízdenek v daný den. Toto obchodní pravidlo pomáhá trochu zjednodušit naši logiku a vyhnout se tak potřebě rozřazovací jednotky k vytváření deterministických průběžných součtů.

Pokud máte případy, kdy můžete mít více řádků pro libovolnou kombinaci LicenseNumber + IncidentDate, můžete remízu přerušit pomocí jiného sloupce, který pomůže, aby byla kombinace jedinečná (samozřejmě zdrojová tabulka již nebude mít jedinečné omezení pro tyto dva sloupce). . Všimněte si, že je to možné i v případech, kdy je DATE sloupec je ve skutečnosti DATETIME – mnoho lidí předpokládá, že hodnoty data/času jsou jedinečné, ale rozhodně to není vždy zaručeno, bez ohledu na podrobnosti.

V mém případě bych mohl použít IDENTITY sloupec IncidentID; takto bych upravil každé řešení (uznávám, že mohou existovat lepší způsoby; jen vyhazuji nápady):

/* --------- dílčí dotaz #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 -- přidán tento řádek:AND t1.IncidentID>=t2.IncidentID,tcidentDate,tcense BY1Nt1um .TicketAmountORDER BY t1.LicenseNumber, t1.IncidentDate; /* --------- vnější použít --------- */ 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 -- přidáno tento řádek:AND IncidentID <=t1.IncidentID) AS t2GROUP BY t1.t1cidentTickDate,bermount1cidentTickDetumA BY t1.LicenseNumber, t1.IncidentDate; /* --------- SUM() OVER using RANGE --------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER ( PARTICE BY LicenseNumber ORDER BY IncidentDate, IncidentID RANGE BEZ OMEZENÍ PŘEDCHOZÍM -- přidán tento sloupec ^^^^^^^^^^^^ ) OD dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate; /* --------- SUM() OVER using ROWS --------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER (ODDĚLENÍ PODLE ČÍSLA licence ORDER BY IncidentDate, IncidentID ŘÁDKY BEZ OMEZENÍ PŘEDCHOZÍ -- přidán tento sloupec ^^^^^^^^^^^^ ) OD dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate; /* --------- iterace založená na sadě --------- */ DECLARE @x TABLE( -- přidal tento sloupec a udělal z něj 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); -- přidán další sloupec do INSERT/SELECT:INSERT @x(ID události, číslo licence, datum události, počet vstupenek, RunningTotal, rn) SELECT ID události, číslo licence, datum události, počet vstupenek, počet vstupenek, ROW_NUMBER() OVER (ODDĚLENÍ PODLE licenčního čísla ORDER BY IncidentDate , IncidentID) -- a přidali tento rozhodující sloupec -----------------------------^^^^^^^^ ^^^^ OD dbo.SpeedingTickets; -- zbytek řešení iterace založené na množině zůstal nezměněn

Další komplikací, na kterou můžete narazit, je, když nestíháte celou tabulku, ale spíše podmnožinu (řekněme v tomto případě první lednový týden). Budete muset provést úpravy přidáním WHERE klauzule a mějte na paměti tyto predikáty, když máte také korelované poddotazy.


  1. Připojte se k mysql na Amazon EC2 ze vzdáleného serveru

  2. Zahodit cizí klíč, aniž byste znali název omezení?

  3. Jak migrovat databáze na váš Reseller Server

  4. Jak UNHEX() funguje v MariaDB