it-swarm-eu.dev

Běží celkem s počítáním?

Jak název napovídá, potřebuji pomoc s získáním celkového výkonu v T-SQL. Problém je v tom, že částka, kterou musím udělat, je součet počtu:

sum(count (distinct (customers))) 

Řekněme, že kdybych počítal sám, výsledkem by bylo:

Day | CountCustomers
----------------------
5/1  |      1
5/2  |      0
5/3  |      5

Potřebuji výstup se součtem:

Day | RunningTotalCustomers
----------------------
5/1  |      1
5/2  |      1
5/3  |      6

Před použitím metody coalesce jsem provedl běh součtů, ale nikdy s počítáním. Nejsem si jistý, jak to udělat hned, když mám počet.

34
user1465095

Zde je několik metod, které můžete porovnat. Nejprve si připravíme tabulku s nějakými figuríny. Naplňuji to spoustou náhodných dat ze sys.all_columns. No, je to trochu náhodné - ujišťuji se, že data jsou sousedící (což je opravdu důležité pouze pro jednu z odpovědí).

CREATE TABLE dbo.Hits(Day SMALLDATETIME, CustomerID INT);

CREATE CLUSTERED INDEX x ON dbo.Hits([Day]);

INSERT dbo.Hits SELECT TOP (5000) DATEADD(DAY, r, '20120501'),
  COALESCE(ASCII(SUBSTRING(name, s, 1)), 86)
FROM (SELECT name, r = ROW_NUMBER() OVER (ORDER BY name)/10,
       s = CONVERT(INT, RIGHT(CONVERT(VARCHAR(20), [object_id]), 1))
FROM sys.all_columns) AS x;

SELECT 
  Earliest_Day   = MIN([Day]), 
  Latest_Day     = MAX([Day]), 
  Unique_Days    = DATEDIFF(DAY, MIN([Day]), MAX([Day])) + 1, 
  Total_Rows     = COUNT(*)
FROM dbo.Hits;

Výsledek:

Earliest_Day         Latest_Day           Unique_Days  Total_Days
-------------------  -------------------  -----------  ----------
2012-05-01 00:00:00  2013-09-13 00:00:00  501          5000

Data vypadají takto (5 000 řádků) - ve vašem systému se však budou mírně lišit v závislosti na verzi a verzi #:

Day                  CustomerID
-------------------  ---
2012-05-01 00:00:00  95
2012-05-01 00:00:00  97
2012-05-01 00:00:00  97
2012-05-01 00:00:00  117
2012-05-01 00:00:00  100
...
2012-05-02 00:00:00  110
2012-05-02 00:00:00  110
2012-05-02 00:00:00  95
...

A výsledky běžných součtů by měly vypadat takto (501 řádků):

Day                  c   rt
-------------------  --  --
2012-05-01 00:00:00  6   6
2012-05-02 00:00:00  5   11
2012-05-03 00:00:00  4   15
2012-05-04 00:00:00  7   22
2012-05-05 00:00:00  6   28
...

Metody, které se chystám porovnat, jsou:

  • „self-join“ - puristický přístup založený na sadě
  • „rekurzivní CTE s daty“ - závisí to na sousedních datech (bez mezer)
  • "rekurzivní CTE s číslem řádku" - podobné výše, ale pomaleji, spoléhající se na ROW_NUMBER
  • „rekurzivní CTE s #temp stolem“ - ukradl Mikaelinu odpověď, jak bylo navrženo
  • „nepředvídatelná aktualizace“, která, i když není podporována a neslibuje definované chování, se zdá být docela populární
  • "kurzor"
  • SQL Server 2012 pomocí nové funkce okna

self-join

To je způsob, jak vám lidé řeknou, abyste to udělali, když vás varují, abyste se drželi dál od kurzoru, protože „nastavení je vždy rychlejší“. V některých nedávných experimentech jsem zjistil, že kurzor toto řešení překonává.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], g.c, rt = SUM(g2.c)
  FROM g INNER JOIN g AS g2
  ON g.[Day] >= g2.[Day]
GROUP BY g.[Day], g.c
ORDER BY g.[Day];

rekurzivní cte s daty

Připomenutí - závisí to na sousedních datech (bez mezer), až na 10 000 úrovních rekurze a že znáte počáteční datum rozsahu, který vás zajímá (pro nastavení kotvy). Kotvu byste mohli dynamicky nastavit pomocí poddotazu, ale já jsem chtěl věci udržet jednoduché.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], c, rt = c
        FROM g
        WHERE [Day] = '20120501'
    UNION ALL
    SELECT g.[Day], g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.[Day] = DATEADD(DAY, 1, x.[Day])
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

rekurzivní cte s číslem řádku

Výpočet čísla řádku je zde poněkud drahý. Opět to podporuje maximální úroveň rekurze 10 000, ale nemusíte přidělit kotvu.

;WITH g AS 
(
  SELECT [Day], rn = ROW_NUMBER() OVER (ORDER BY DAY), 
    c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], rn, c, rt = c
        FROM g
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

rekurzivní cte s dočasnou tabulkou

Ukradl Mikaelinu odpověď, jak bylo navrženo, aby to zahrnul do testů.

CREATE TABLE #Hits
(
  rn INT PRIMARY KEY,
  c INT,
  [Day] SMALLDATETIME
);

INSERT INTO #Hits (rn, c, Day)
SELECT ROW_NUMBER() OVER (ORDER BY DAY),
       COUNT(DISTINCT CustomerID),
       [Day]
FROM dbo.Hits
GROUP BY [Day];

WITH x AS
(
    SELECT [Day], rn, c, rt = c
        FROM #Hits as c
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN #Hits as g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

DROP TABLE #Hits;

nepředvídatelná aktualizace

Znovu to zahrnuji pouze pro úplnost; Já osobně bych se spoléhat na toto řešení, protože, jak jsem se zmínil o jiné odpovědi, tato metoda není zaručeno, že bude fungovat vůbec, a může zcela zlomit v budoucí verzi serveru SQL. (Snažím se donutit SQL Server, aby poslouchal pořadí, které chci, pomocí nápovědy pro volbu indexu.)

CREATE TABLE #x([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x([Day]);

INSERT #x([Day], c) 
    SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt1 INT;
SET @rt1 = 0;

UPDATE #x
SET @rt1 = rt = @rt1 + c
FROM #x WITH (INDEX = x);

SELECT [Day], c, rt FROM #x ORDER BY [Day];

DROP TABLE #x;

kurzor

"Pozor, tady jsou kurzory! Kurzory jsou zlí! Měli byste se kurzory vyhnout za každou cenu!" Ne, to nemluvím já, je to jen něco, co hodně slyším. Na rozdíl od všeobecného mínění jsou v některých případech vhodné kurzory.

CREATE TABLE #x2([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x2([Day]);

INSERT #x2([Day], c) 
    SELECT [Day], COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt2 INT, @d SMALLDATETIME, @c INT;
SET @rt2 = 0;

DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  FOR SELECT [Day], c FROM #x2 ORDER BY [Day];

OPEN c;

FETCH NEXT FROM c INTO @d, @c;

WHILE @@FETCH_STATUS = 0
BEGIN
  SET @rt2 = @rt2 + @c;
  UPDATE #x2 SET rt = @rt2 WHERE [Day] = @d;
  FETCH NEXT FROM c INTO @d, @c;
END

SELECT [Day], c, rt FROM #x2 ORDER BY [Day];

DROP TABLE #x2;

SQL Server 2012

Pokud jste na nejnovější verzi serveru SQL, vylepšení funkčnosti okna nám umožňuje snadno vypočítat průběžné součty bez exponenciálních nákladů na vlastní spojení (součet se počítá jedním průchodem), složitost CTE (včetně požadavku) sousedících řádků pro lepší výkon CTE), nepodporovaná nepředvídatelná aktualizace a zakázaný kurzor. Jen si dejte pozor na rozdíl mezi použitím RANGE a ROWS, nebo nešpecifikováním vůbec - pouze ROWS se vyhne cívce na disku, což jinak výrazně zhorší výkon.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], c, 
  rt = SUM(c) OVER (ORDER BY [Day] ROWS UNBOUNDED PRECEDING)
FROM g
ORDER BY g.[Day];

porovnání výkonu

Vzal jsem každý přístup a zabalil jsem to šarži pomocí následujícího:

SELECT SYSUTCDATETIME();
GO
DBCC DROPCLEANBUFFERS;DBCC FREEPROCCACHE;
-- query here
GO 10
SELECT SYSUTCDATETIME();

Zde jsou výsledky celkové doby trvání v milisekundách (pamatujte, že to zahrnuje pokaždé také příkazy DBCC):

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1296 ms   1357 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1655 ms   1516 ms
recursive cte with row_number   19747 ms  19630 ms
recursive cte with #temp table   1624 ms   1329 ms
quirky update                     880 ms   1030 ms -- non-SQL 2012 winner
cursor                           1962 ms   1850 ms
SQL Server 2012                   847 ms    917 ms -- winner if SQL 2012 available

A udělal jsem to znovu bez příkazů DBCC:

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1272 ms   1309 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1247 ms   1593 ms
recursive cte with row_number   18646 ms  18803 ms
recursive cte with #temp table   1340 ms   1564 ms
quirky update                    1024 ms   1116 ms -- non-SQL 2012 winner
cursor                           1969 ms   1835 ms
SQL Server 2012                   600 ms    569 ms -- winner if SQL 2012 available

Odstranění jak DBCC, tak i smyček, pouze změřením jedné raw iterace:

method                          run 1     run 2
-----------------------------   --------  --------
self-join                         313 ms    242 ms
recursive cte with dates          217 ms    217 ms
recursive cte with row_number    2114 ms   1976 ms
recursive cte with #temp table     83 ms    116 ms -- "supported" non-SQL 2012 winner
quirky update                      86 ms     85 ms -- non-SQL 2012 winner
cursor                           1060 ms    983 ms
SQL Server 2012                    68 ms     40 ms -- winner if SQL 2012 available

Nakonec jsem vynásobil počet řádků ve zdrojové tabulce 10 (změna na 50000 a přidání další tabulky jako křížové spojení). Výsledky této jediné iterace bez příkazů DBCC (jednoduše v zájmu času):

method                           run 1      run 2
-----------------------------    --------   --------
self-join                         2401 ms    2520 ms
recursive cte with dates           442 ms     473 ms
recursive cte with row_number   144548 ms  147716 ms
recursive cte with #temp table     245 ms     236 ms -- "supported" non-SQL 2012 winner
quirky update                      150 ms     148 ms -- non-SQL 2012 winner
cursor                            1453 ms    1395 ms
SQL Server 2012                    131 ms     133 ms -- winner

Měřil jsem pouze délku - nechám to jako cvičení pro čtenáře, aby porovnal tyto přístupy ke svým datům a porovnal jiné metriky, které mohou být důležité (nebo se mohou lišit podle jejich schématu/dat). Předtím, než z této odpovědi vyvodíte nějaké závěry, bude na vás, abyste ji otestovali proti svým datům a schématu ... tyto výsledky se téměř jistě změní, jak se počet řádků zvýší.


demo

Přidal jsem sqlfiddle . Výsledek:

enter image description here


závěr

V mých testech by byla volba:

  1. Metoda SQL Server 2012, pokud mám k dispozici SQL Server 2012.
  2. Pokud SQL Server 2012 není k dispozici a moje data jsou sousedící, půjdu s metodou rekurzivní cte s daty.
  3. Pokud by nebylo možné použít ani 1., ani 2., tak bych se spojil s nepředvídatelnou aktualizací, i když byl výkon blízko, jen proto, že chování je dokumentováno a zaručeno. Mám menší starosti s budoucí kompatibilitou, protože doufejme, že pokud nepředvídatelná aktualizace skončí, bude to poté, co již převedu celý svůj kód na 1. :-)

Ale znovu, měli byste je otestovat podle schématu a dat. Protože to byl vymyšlený test s relativně nízkým počtem řádků, může to být také prd ve větru. Udělal jsem další testy s různými počty schémat a řádků a výkonnostní heuristika byla docela odlišná ... proto jsem na vaši původní otázku položil tolik následných otázek.


[~ # ~] aktualizace [~ # ~]

Více o tomto blogu jsem zde:

Nejlepší přístupy pro spouštění součtů - aktualizováno pro SQL Server 2012

53
Aaron Bertrand

Toto je zřejmě optimální řešení

DECLARE @dailyCustomers TABLE (day smalldatetime, CountCustomers int, RunningTotal int)

DECLARE @RunningTotal int

SET @RunningTotal = 0

INSERT INTO @dailyCustomers 
SELECT day, CountCustomers, null
FROM Sales
ORDER BY day

UPDATE @dailyCustomers
SET @RunningTotal = RunningTotal = @RunningTotal + CountCustomers
FROM @dailyCustomers

SELECT * FROM @dailyCustomers
1
Code Magician