Toplam sayımla mı çalışıyorsun?


34

Başlıktan da anlaşılacağı gibi T-SQL'de çalışan toplam almak için biraz yardıma ihtiyacım var. Sorun şu ki, yapmam gereken miktar bir sayının toplamı:

sum(count (distinct (customers))) 

Sayımı tek başıma koyarsam, sonuç şöyle olur:

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

Toplam olması için çıktıya ihtiyacım var:

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

coalesceYöntemi kullanmadan önce toplamı çalıştırdım , ama asla sayılmaz. Artık sayım olduğundan nasıl yapacağımı bilmiyorum.


2
Hangi SQL Server sürümü lütfen? Verilerin kapsamını paylaşabilir misiniz - 1000 satır, bir milyon, bir milyardan mı bahsediyoruz? Gerçekten sadece bu iki sütun mu, yoksa şemayı bizim için kolaylaştırdın mı? Son olarak, Daybir anahtardır ve değerler bitişik midir?
Aaron Bertrand

Ben toplam (İmleç vs Hibrid özyinelemeli CTE vs İlginç güncellemesini) çalıştırma hakkında kapsamlı bir blog yaptı: ienablemuch.com/2012/05/... Ben kullandığı saf seti tabanlı yaklaşım, performans olmak başka bir şey olduğunu Çalışan toplamı içermiyordu İstenilen: sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen

Yanıtlar:


53

İşte karşılaştırabileceğiniz birkaç yöntem. İlk önce bazı yapay verileri içeren bir tablo hazırlayalım. Bunu, sys.all_columns dosyasındaki bir grup rasgele veriyle dolduruyorum. Eh, biraz rastgele - Tarihlerin bitişik olmasını sağlıyorum (bu, cevapların yalnızca biri için gerçekten önemlidir).

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;

Sonuçlar:

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

Veriler şöyle gözüküyor (5000 satır) - ancak sürüme ve yapı #'ye bağlı olarak sisteminizde biraz farklı görünecek:

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
...

Akan toplamlar sonuçları şöyle görünmelidir (501 satır):

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
...

Yani karşılaştıracağım yöntemler:

  • "öz-birleşme" - kümeye dayalı sadelik yaklaşımı
  • "tarihleri ​​olan özyinelemeli CTE" - bu bitişik tarihlere dayanır (boşluk yok)
  • "satır_numaralı özyinelemeli CTE" - yukarıdakine benzer ancak daha yavaş, ROW_NUMBER
  • "#temp tablosuyla özyinelemeli CTE" - önerildiği gibi Mikael'in cevabından çalındı
  • Desteklenmeyen ve gelecek vaad eden bir davranış sergilemediği halde, "ilginç güncelleme" oldukça popüler görünüyor
  • "İmleç"
  • Yeni pencereleme işlevini kullanarak SQL Server 2012

iç birleşim

İnsanların sizi imleçlerden uzak durmaları konusunda uyardıklarında yapmaları gerektiğini söyleyeceği yol budur, çünkü “set tabanlı her zaman daha hızlıdır”. Bazı yeni deneylerde imlecin bu çözümü geride bıraktığını gördüm.

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

tarihleri ​​ile özyinelemeli cte

Hatırlatma - Bu, bitişik tarihlere (boşluksuz), 10000'e kadar özyinelemeye dayanır ve ilgilendiğiniz aralığın başlangıç ​​tarihini bildiğinize (çapayı ayarlamak için) dayanır. Çapayı dinamik olarak bir alt sorgu kullanarak ayarlayabilirsin, ama işleri basit tutmak istedim.

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

row_number ile özyinelemeli cte

Row_number hesaplama burada biraz pahalı. Yine bu, 10000'lük maksimum özyinelemeyi destekler, ancak çapa atamanıza gerek yoktur.

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

temp tablo ile özyinelemeli cte

Mikael'in cevabından, önerildiği gibi, bunu testlere dahil etmek için çalmak.

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;

ilginç güncelleme

Yine bunu sadece bütünlük için dahil ediyorum; Şahsen bu çözüme güvenmezdim, çünkü başka bir cevapta da belirttiğim gibi, bu yöntemin hiç çalışması garanti edilmiyor ve SQL Server'ın gelecekteki bir sürümünde tamamen bozulabilir. (Dizin seçimi için bir ipucu kullanarak, SQL Server'ı istediğim sıraya uymaya zorlamak için elimden geleni yapıyorum.)

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;

kürsör

"Dikkat et, burada imleçler var! İmleçler kötü! İmleçlerden her ne pahasına olursa olsun kaçınmalısın!" Hayır, o konuşmuyorum, sadece çok duyduğum şeyler. Popüler düşüncenin aksine, imleclerin uygun olduğu bazı durumlar vardır.

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

SQL Server'ın en yeni sürümündeyseniz, pencereleme işlevindeki iyileştirmeler, kendi kendine katılma maliyetinin katlanmadan (SUM bir seferde hesaplanır), CTE'lerin karmaşıklığı (gereklilik dahil) hesaplanmasını sağlar. daha iyi performans gösteren CTE için bitişik satırların listesi, desteklenmeyen ilginç güncelleme ve yasak imleç. Sadece kullanma RANGEve ROWShiçbir şekilde belirtmeme arasındaki farka dikkat edin - ROWSaksi takdirde performansı önemli ölçüde azaltacak olan disk üstü bir makaradan kaçınır.

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

performans karşılaştırmaları

Her yaklaşımı alıp aşağıdakileri kullanarak bir seriyi tamamladım:

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

Toplam sürenin milisaniye cinsinden sonuçları aşağıdadır (bunun her seferinde DBCC komutlarını da içerdiğini unutmayın):

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

DBCC komutları olmadan tekrar yaptım:

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

Hem DBCC hem de loop'ları çıkarmak, sadece bir ham yinelemeyi ölçmek:

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

Son olarak, kaynak tablodaki satır sayısını 10'la çarptım (en üst 50000'e değiştirerek ve çapraz birleştirme olarak başka bir tablo ekleyerek). Bunun sonucu, DBCC komutları olmayan tek bir yineleme (sadece zamanın çıkarları doğrultusunda):

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

Sadece süreyi ölçtüm - Okuyucunun verilerini bu yaklaşımları karşılaştırmak, önemli olabilecek (veya şemalarına / verisine göre değişebilir) diğer ölçümleri karşılaştırmak için bir egzersiz olarak bırakacağım. Bu cevaptan herhangi bir sonuç çıkarmadan önce, verilerinize ve şemalarınıza göre test etmek size kalmıştır ... bu sonuçlar, satır sayıları yükseldikçe neredeyse kesinlikle değişecektir.


gösteri

Bir sqlfiddle ekledim . Sonuçlar:

görüntü tanımını buraya girin


Sonuç

Testlerimde seçim şöyle olacaktır:

  1. Kullanılabilir SQL Server 2012 varsa, SQL Server 2012 yöntemi.
  2. SQL Server 2012 mevcut değilse ve tarihlerim bitişikse, özyinelemeli cte ile tarihler yöntemiyle giderdim.
  3. Ne 1. ne de 2. geçerli değilse, performansın yakın olmasına rağmen, sadece davranışlar belgelendiğinden ve garanti edildiğinden dolayı, ilginç güncelleme üzerinden kendi kendime katılmaya giderdim. Gelecekteki uyumluluk konusunda daha az endişeliyim, çünkü umarım ilginç güncelleme bozulursa tüm kodumu zaten 1'e dönüştürdükten sonra olur. :-)

Fakat yine de, bunları şema ve verilerinize karşı test etmelisiniz. Bu, nispeten düşük sıra sayımlı bir test olduğundan, rüzgarda da osuruk olabilir. Farklı şema ve satır sayıları ile başka testler yaptım ve performans sezgiselliği oldukça farklıydı ... bu yüzden asıl sorunuza birçok takip sorusu sordum.


GÜNCELLEME

Bununla ilgili daha fazla blog yazdım:

Toplamları çalıştırmak için en iyi yaklaşımlar - SQL Server 2012 için güncellendi


1

Bu, görünüşe göre, en uygun çözümdür

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

Bir geçici tabloyu uygulamadan herhangi bir fikir (proc zaten gerekli olması nedeniyle birkaç geçici tabloya değerleri zorluyor, bu yüzden başka bir geçici tabloyu kullanmaktan kaçınmanın bir yolunu bulmaya çalışıyorum)? Olmazsa, bu yöntemi kullanacağım. Bence işe yarayacak

Kendi kendine birleştirme veya iç içe geçmiş bir alt sorgu ile de yapılabilir, ancak bu seçenekler neredeyse hiç başarılı olmaz. Ayrıca, bazı biriktirme ya da çalışma masaları ile bu alternatiflerle yine de tempdb'ye varacaksınız.

3
Sadece bu "ilginç güncelleme" yönteminin çalışmasının garanti edilmediğinin farkında olun - bu sözdizimi desteklenmez ve davranışı tanımsızdır ve gelecekteki bir sürümde, düzeltme ya da servis paketinde bozulabilir. Bu nedenle, evet, desteklenen bazı alternatiflerden daha hızlı olsa da, gelecekteki potansiyel bir uyumluluk maliyeti söz konusudur.
Aaron Bertrand

6
Jeff Moden'in bir yerde yazdığı bu yaklaşıma dair birçok uyarı var. dayÖrneğin , kümelenmiş bir dizine sahip olmalısınız .
Martin Smith

2
@MartinSmith sqlservercentral.com adresinde ÇOK BÜYÜK bir makaledir (Yazar sayfasına gidin ve 'ilginç güncellemeler hakkındaki makalelerini bulun).
Fabricio Araujo,

-2

Başka bir yol, pahalı, ancak sürüm bağımsız. Temp tabloları veya değişkenleri kullanmaz.

select T.dday, T.CustomersByDay + 
    (select count(A.customer) from NewCustomersByDate A 
      where A.dday < T.dday) as TotalCustomerTillNow 
from (select dday, count(customer) as CustomersByDay 
        from NewCustomersByDate group by dday) T 

2
Bu iyi değil, çok yavaş. Sadece 100 satıra sahip olsanız bile, 5.050 defada masalar arasında ping-pong okuyacak. 200 satır, 20,100 katıdır. Sadece 1000 satır ile, katlanarak 500,500 atlar okur sqlblog.com/blogs/adam_machanic/archive/2006/07/12/...
Michael Buen

Bunu gönderdikten sonra blogunuza giden bağlantıyı gördüm, şimdi bunun çok kötü bir fikir olduğunu gördüm, teşekkürler!
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.