Berjalan total dengan jumlah?

34

Seperti judulnya saya butuh bantuan untuk mendapatkan total berjalan dalam T-SQL. Masalahnya adalah jumlah yang harus saya lakukan adalah jumlah hitungan:

sum(count (distinct (customers))) 

Katakanlah jika saya menghitung sendiri, hasilnya adalah:

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

Saya perlu output dengan jumlah menjadi:

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

Saya sudah selesai menjalankan total sebelum menggunakan coalescemetode ini, tetapi tidak pernah dengan hitungan. Saya tidak yakin bagaimana melakukannya sekarang karena saya memiliki hitungan.

Aaron Bertrand
sumber
2
Tolong apa versi SQL Server? Bisakah Anda berbagi ruang lingkup data - apakah kita berbicara tentang 1000 baris, satu juta, satu miliar? Apakah ini hanya dua kolom ini saja, atau sudahkah Anda menyederhanakan skema untuk kami? Akhirnya, apakah Daykuncinya, dan apakah nilainya berdekatan?
Aaron Bertrand
Saya membuat blog yang komprehensif tentang menjalankan total (Pembaruan unik vs Hybrid Recursive CTE vs Kursor): ienablemuch.com/2012/05/... Saya tidak memasukkan total berjalan yang menggunakan pendekatan berbasis set murni, kinerja tidak ada artinya diinginkan: sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen

Jawaban:

53

Berikut adalah beberapa metode yang dapat Anda bandingkan. Pertama mari kita buat tabel dengan beberapa data dummy. Saya mengisi ini dengan banyak data acak dari sys.all_columns. Yah, ini agak acak - saya memastikan bahwa tanggalnya berdekatan (yang benar-benar hanya penting untuk salah satu jawaban).

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;

Hasil:

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

Data terlihat seperti ini (5000 baris) - tetapi akan terlihat sedikit berbeda pada sistem Anda tergantung pada versi dan versi #:

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

Dan hasil total yang berjalan akan terlihat seperti ini (501 baris):

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

Jadi metode yang akan saya bandingkan adalah:

  • "bergabung sendiri" - pendekatan purist berbasis set
  • "CTE rekursif dengan tanggal" - ini bergantung pada tanggal yang berdekatan (tanpa celah)
  • "CTE rekursif dengan row_number" - mirip dengan di atas tetapi lebih lambat, mengandalkan ROW_NUMBER
  • "CTE rekursif dengan tabel #temp" - dicuri dari jawaban Mikael seperti yang disarankan
  • "pembaruan unik" yang, walaupun tidak didukung dan tidak menjanjikan perilaku yang ditentukan, tampaknya cukup populer
  • "kursor"
  • SQL Server 2012 menggunakan fungsi windowing baru

bergabung sendiri

Ini adalah cara orang akan mengatakan kepada Anda untuk melakukannya ketika mereka memperingatkan Anda untuk menjauh dari kursor, karena "berbasis set selalu lebih cepat." Dalam beberapa percobaan baru-baru ini saya telah menemukan bahwa kursor melampaui solusi ini.

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

CTE rekursif dengan tanggal

Pengingat - ini bergantung pada tanggal yang berdekatan (tanpa celah), hingga 10.000 tingkat rekursi, dan Anda tahu tanggal mulai rentang yang Anda minati (untuk mengatur jangkar). Anda dapat mengatur jangkar secara dinamis menggunakan subquery, tentu saja, tetapi saya ingin menyederhanakannya.

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

cte rekursif dengan row_number

Perhitungan Row_number sedikit mahal di sini. Sekali lagi ini mendukung level maksimum rekursi 10000, tetapi Anda tidak perlu menetapkan jangkar.

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

cte rekursif dengan tabel temp

Mencuri dari jawaban Mikael, seperti yang disarankan, untuk memasukkan ini dalam tes.

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;

pembaruan yang unik

Sekali lagi saya hanya memasukkan ini untuk kelengkapan; Saya pribadi tidak akan bergantung pada solusi ini karena, seperti yang saya sebutkan pada jawaban lain, metode ini tidak dijamin berfungsi sama sekali, dan mungkin benar-benar merusak versi SQL Server yang akan datang. (Saya melakukan yang terbaik untuk memaksa SQL Server mematuhi perintah yang saya inginkan, menggunakan petunjuk untuk pilihan indeks.)

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;

kursor

"Waspadalah, ada kursor di sini! Kursor itu jahat! Kamu harus menghindari kursor di semua biaya!" Tidak, itu bukan saya yang berbicara, itu hanya hal yang saya dengar banyak. Berlawanan dengan pendapat umum, ada beberapa kasus di mana kursor sesuai.

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

Jika Anda menggunakan SQL Server versi terbaru, peningkatan fungsionalitas windowing memungkinkan kami untuk menghitung total running dengan mudah tanpa biaya eksponensial dari self-gabung (SUM dihitung dalam satu pass), kompleksitas CTE (termasuk persyaratan) baris yang berdekatan untuk CTE yang berkinerja lebih baik), pembaruan unik yang tidak didukung, dan kursor terlarang. Berhati-hatilah dengan perbedaan antara menggunakan RANGEdan ROWS, atau tidak menentukan sama sekali - hanya ROWSmenghindari spool pada disk, yang akan menghambat kinerja secara signifikan.

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

perbandingan kinerja

Saya mengambil setiap pendekatan dan membungkusnya menggunakan berikut ini:

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

Berikut adalah hasil dari total durasi, dalam milidetik (ingat ini termasuk perintah DBCC setiap kali juga):

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

Dan saya melakukannya lagi tanpa perintah 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

Menghapus DBCC dan loop, hanya mengukur satu iterasi mentah:

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

Akhirnya, saya mengalikan jumlah baris dalam tabel sumber dengan 10 (mengubah atas menjadi 50.000 dan menambahkan tabel lain sebagai gabungan silang). Hasil ini, satu iterasi tunggal tanpa perintah DBCC (hanya untuk kepentingan waktu):

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

Saya hanya mengukur durasi - Saya akan menyerahkannya kepada pembaca untuk membandingkan pendekatan ini pada data mereka, membandingkan metrik lain yang mungkin penting (atau mungkin berbeda dengan skema / data mereka). Sebelum menarik kesimpulan apa pun dari jawaban ini, terserah Anda untuk mengujinya terhadap data dan skema Anda ... hasil ini hampir pasti akan berubah karena jumlah baris semakin tinggi.


demo

Saya telah menambahkan sqlfiddle . Hasil:

masukkan deskripsi gambar di sini


kesimpulan

Dalam tes saya, pilihannya adalah:

  1. Metode SQL Server 2012, jika saya memiliki SQL Server 2012 tersedia.
  2. Jika SQL Server 2012 tidak tersedia, dan tanggal saya berdekatan, saya akan pergi dengan cyt rekursif dengan metode tanggal.
  3. Jika 1. atau 2. tidak berlaku, saya akan bergabung dengan self-join atas pembaruan unik, meskipun kinerjanya dekat, hanya karena perilaku tersebut didokumentasikan dan dijamin. Saya kurang khawatir tentang kompatibilitas di masa depan karena mudah-mudahan jika pembaruan yang unik istirahat itu akan setelah saya sudah mengkonversi semua kode saya ke 1. :-)

Tetapi sekali lagi, Anda harus menguji ini terhadap skema dan data Anda. Karena ini adalah tes yang dibuat dengan jumlah baris yang relatif rendah, itu mungkin juga kentut dalam angin. Saya telah melakukan tes lain dengan skema dan jumlah baris yang berbeda, dan heuristik kinerja sangat berbeda ... itulah sebabnya saya mengajukan begitu banyak pertanyaan lanjutan ke pertanyaan awal Anda.


MEMPERBARUI

Saya telah membuat blog lebih banyak tentang ini di sini:

Pendekatan terbaik untuk menjalankan total - diperbarui untuk SQL Server 2012

Aaron Bertrand
sumber
1

Ini, tampaknya, adalah solusi optimal

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
Penyihir Kode
sumber
Adakah ide tanpa menerapkan tabel temp (proc saya sudah memaksa nilai melalui beberapa tabel temp karena kebutuhan, jadi saya mencoba mencari cara untuk menghindari menggunakan tabel temp lainnya)? Jika tidak, saya akan menggunakan metode ini. Saya pikir itu akan berhasil
Ini juga bisa dilakukan dengan bergabung sendiri, atau subquery bersarang tetapi opsi ini tidak berkinerja hampir sama. Kemungkinan besar Anda akan tetap menggunakan tempdb dengan alternatif ini dengan beberapa spooling atau meja kerja.
3
Perlu diketahui bahwa metode "pembaruan unik" ini tidak dijamin berfungsi - sintaks ini tidak didukung dan perilakunya tidak terdefinisi, dan dapat rusak di versi mendatang, perbaikan terbaru atau paket layanan. Jadi sementara ya itu lebih cepat daripada beberapa alternatif yang didukung, yang datang dengan potensi biaya kompatibilitas di masa depan.
Aaron Bertrand
6
Ada banyak peringatan untuk pendekatan ini yang ditulis Jeff Moden di suatu tempat. Anda harus mengaktifkan indeks berkerumundayMisalnya .
Martin Smith
2
@MartinSmith Ini adalah artikel yang SANGAT BESAR di sqlservercentral.com (buka halaman Penulis dan temukan artikelnya tentang pembaruan quirck).
Fabricio Araujo
-2

Hanya cara lain, mahal, tetapi versi independen. Itu tidak menggunakan tabel temp atau variabel.

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 

sumber
2
Itu tidak baik, itu sangat lambat. Bahkan Anda hanya memiliki 100 baris, itu akan melakukan ping-pong membaca antar tabel di 5.050 kali. 200 baris, adalah 20.100 kali. Dengan 1.000 baris saja, ini melonjak secara eksponensial menjadi 500.500 kali dibaca sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen
Saya melihat tautan ke blog Anda setelah memposting ini, sekarang saya melihat ini adalah ide yang sangat buruk, terima kasih!