Chạy tổng cộng với số lượng?


34

Như tiêu đề cho thấy tôi cần một số trợ giúp để có được tổng số hoạt động trong T-SQL. Vấn đề là tổng số tôi cần làm là tổng của một số đếm:

sum(count (distinct (customers))) 

Nói rằng nếu tôi chạy số đếm một mình, kết quả sẽ là:

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

Tôi cần đầu ra với tổng là:

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

Tôi đã thực hiện chạy tổng số trước khi sử dụng coalescephương thức, nhưng không bao giờ đếm được. Tôi không chắc làm thế nào để làm điều đó bây giờ tôi có số lượng.


2
Phiên bản SQL Server nào vui lòng? Bạn có thể chia sẻ phạm vi của dữ liệu - chúng ta đang nói về 1000 hàng, một triệu, một tỷ? Có thực sự chỉ là hai cột này, hoặc bạn đã đơn giản hóa lược đồ cho chúng tôi? Cuối cùng, là Daymột chìa khóa, và các giá trị có tiếp giáp nhau không?
Aaron Bertrand

Tôi đã tạo một blog toàn diện về việc chạy tổng (cập nhật Quirky so với CTE đệ quy vs Coder ): ienablemuch.com/2012/05/. Tôi không bao gồm tổng số chạy sử dụng phương pháp dựa trên tập hợp thuần túy, hiệu suất không có gì để làm mong muốn: sqlblog.com/blogs/adam_machanic/archive/2006/07/12/...
Michael Buen

Câu trả lời:


53

Dưới đây là một vài phương pháp bạn có thể so sánh. Trước tiên, hãy thiết lập một bảng với một số dữ liệu giả. Tôi đang điền dữ liệu này với một loạt dữ liệu ngẫu nhiên từ sys.all_columns. Chà, thật là ngẫu nhiên - Tôi đảm bảo rằng ngày tháng không liên tục (điều này thực sự chỉ quan trọng đối với một trong những câu trả lời).

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;

Các kết quả:

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

Dữ liệu trông như thế này (5000 hàng) - nhưng sẽ trông hơi khác trên hệ thống của bạn tùy thuộc vào phiên bản và bản dựng #:

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

Và kết quả tổng cộng đang chạy sẽ như thế này (501 hàng):

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

Vì vậy, các phương pháp tôi sẽ so sánh là:

  • "tự tham gia" - cách tiếp cận thuần túy dựa trên tập hợp
  • "CTE đệ quy có ngày" - điều này phụ thuộc vào ngày tiếp giáp (không có khoảng trống)
  • "CTE đệ quy với row_number" - tương tự như trên nhưng chậm hơn, dựa vào ROW_NUMBER
  • "CTE đệ quy với bảng #temp" - bị đánh cắp từ câu trả lời của Mikael như đề xuất
  • "cập nhật kỳ quặc", trong khi hành vi được xác định không được hỗ trợ và không hứa hẹn, dường như khá phổ biến
  • "con trỏ"
  • SQL Server 2012 sử dụng chức năng cửa sổ mới

tự tham gia

Đây là cách mọi người sẽ bảo bạn làm điều đó khi họ cảnh báo bạn tránh xa những con trỏ, bởi vì "dựa trên tập hợp luôn nhanh hơn". Trong một số thí nghiệm gần đây, tôi thấy rằng con trỏ vượt quá giải pháp này.

;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 đệ quy với ngày

Nhắc nhở - điều này phụ thuộc vào các ngày liền kề (không có khoảng trống), lên tới 10000 mức đệ quy và bạn biết ngày bắt đầu của phạm vi bạn quan tâm (để đặt neo). Dĩ nhiên, bạn có thể thiết lập neo một cách linh hoạt bằng cách sử dụng truy vấn con, nhưng tôi muốn giữ mọi thứ đơn giản.

;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 đệ quy với row_number

Tính toán Row_number hơi đắt ở đây. Một lần nữa, điều này hỗ trợ mức đệ quy tối đa 10000, nhưng bạn không cần gán neo.

;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 đệ quy với bảng tạm thời

Ăn cắp từ câu trả lời của Mikael, như được đề xuất, để đưa điều này vào các bài kiểm tra.

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;

cập nhật kỳ quặc

Một lần nữa tôi chỉ bao gồm điều này cho sự hoàn chỉnh; Cá nhân tôi sẽ không dựa vào giải pháp này vì như tôi đã đề cập trong một câu trả lời khác, phương pháp này hoàn toàn không đảm bảo để hoạt động và hoàn toàn có thể phá vỡ phiên bản SQL Server trong tương lai. (Tôi đang cố hết sức để ép buộc SQL Server tuân theo thứ tự tôi muốn, sử dụng một gợi ý cho lựa chọn chỉ mục.)

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;

con trỏ

"Coi chừng, có con trỏ ở đây! Con trỏ là ác quỷ! Bạn nên tránh con trỏ bằng mọi giá!" Không, đó không phải là tôi nói, đó chỉ là thứ tôi nghe được nhiều. Trái với ý kiến ​​phổ biến, có một số trường hợp con trỏ thích hợp.

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;

Máy chủ SQL 2012

Nếu bạn đang sử dụng phiên bản SQL Server mới nhất, việc cải tiến chức năng cửa sổ cho phép chúng tôi dễ dàng tính toán tổng số chạy mà không phải trả chi phí theo cấp số nhân (SUM được tính trong một lần), độ phức tạp của CTE (bao gồm cả yêu cầu các hàng liền kề để CTE hoạt động tốt hơn), cập nhật kỳ quặc không được hỗ trợ và con trỏ bị cấm. Chỉ cần cảnh giác về sự khác biệt giữa việc sử dụng RANGEROWShoặc không chỉ định tất cả - chỉ ROWStránh một bộ đệm trên đĩa, điều này sẽ cản trở hiệu suất đáng kể nếu không.

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

so sánh hiệu suất

Tôi đã thực hiện từng cách tiếp cận và gói nó một đợt bằng cách sử dụng như sau:

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

Dưới đây là kết quả của tổng thời lượng, tính bằng mili giây (hãy nhớ điều này bao gồm cả các lệnh DBCC mỗi lầ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

Và tôi đã làm điều đó một lần nữa mà không có lệnh 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

Loại bỏ cả DBCC và các vòng lặp, chỉ cần đo một lần lặp thô:

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

Cuối cùng, tôi đã nhân số đếm hàng trong bảng nguồn với 10 (thay đổi hàng đầu thành 50000 và thêm một bảng khác dưới dạng nối chéo). Kết quả của việc này, một lần lặp duy nhất không có lệnh DBCC (đơn giản là vì lợi ích của thời gian):

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

Tôi chỉ đo thời lượng - Tôi sẽ để nó như một bài tập cho người đọc để so sánh các cách tiếp cận này trên dữ liệu của họ, so sánh các số liệu khác có thể quan trọng (hoặc có thể thay đổi theo lược đồ / dữ liệu của họ). Trước khi đưa ra bất kỳ kết luận nào từ câu trả lời này, bạn sẽ phải kiểm tra nó dựa trên dữ liệu và lược đồ của bạn ... những kết quả này gần như chắc chắn sẽ thay đổi khi số lượng hàng tăng cao hơn.


bản giới thiệu

Tôi đã thêm một sqlfiddle . Các kết quả:

nhập mô tả hình ảnh ở đây


phần kết luận

Trong các thử nghiệm của tôi, sự lựa chọn sẽ là:

  1. Phương pháp SQL Server 2012, nếu tôi có sẵn SQL Server 2012.
  2. Nếu SQL Server 2012 không có sẵn và ngày của tôi liền kề nhau, tôi sẽ sử dụng phương thức đệ quy với phương thức ngày.
  3. Nếu không áp dụng 1. và 2., tôi sẽ tự tham gia vào bản cập nhật kỳ quặc, mặc dù hiệu suất đã gần, chỉ vì hành vi được ghi lại và đảm bảo. Tôi ít lo lắng hơn về khả năng tương thích trong tương lai vì hy vọng nếu bản cập nhật kỳ quặc sẽ bị hỏng sau khi tôi đã chuyển đổi tất cả mã của mình thành 1 .:-)

Nhưng một lần nữa, bạn nên kiểm tra chúng dựa trên lược đồ và dữ liệu của bạn. Vì đây là một thử nghiệm khó khăn với số lượng hàng tương đối thấp, nó cũng có thể là một cái rắm trong gió. Tôi đã thực hiện các thử nghiệm khác với các lược đồ và số hàng khác nhau và các heuristic hiệu suất hoàn toàn khác nhau ... đó là lý do tại sao tôi đã hỏi rất nhiều câu hỏi tiếp theo cho câu hỏi ban đầu của bạn.


CẬP NHẬT

Tôi đã viết thêm về điều này ở đây:

Các cách tiếp cận tốt nhất để chạy tổng số - được cập nhật cho SQL Server 2012


1

Đây là, rõ ràng, giải pháp tối ưu

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

Bất kỳ ý tưởng nào mà không thực hiện bảng tạm thời (Proc của tôi đã buộc các giá trị thông qua một số bảng tạm thời theo yêu cầu, vì vậy tôi đang cố gắng tìm cách tránh sử dụng bảng tạm thời khác)? Nếu không, tôi sẽ sử dụng phương pháp này. Tôi nghĩ rằng nó sẽ hoạt động

Nó cũng có thể được thực hiện với tự tham gia hoặc truy vấn con lồng nhau nhưng các tùy chọn này không thực hiện gần như tốt. Ngoài ra, có khả năng bạn sẽ nhấn tempdb bằng mọi cách với các lựa chọn thay thế hoặc bàn làm việc.

3
Chỉ cần lưu ý rằng phương pháp "cập nhật kỳ quặc" này không được đảm bảo để hoạt động - cú pháp này không được hỗ trợ và hành vi của nó không được xác định và nó có thể phá vỡ trong phiên bản tương lai, bản sửa lỗi nóng hoặc gói dịch vụ. Vì vậy, trong khi có, nó nhanh hơn một số lựa chọn thay thế được hỗ trợ, đi kèm với chi phí tương thích tiềm năng trong tương lai.
Aaron Bertrand

6
Có rất nhiều sự cẩn thận đối với phương pháp này mà Jeff Moden đã viết ở đâu đó. Bạn nên có một chỉ số cụm trên dayví dụ.
Martin Smith

2
@MartinSmith Đây là một bài viết RẤT LỚN tại sqlservercentral.com (truy cập trang Tác giả và tìm các bài viết của anh ấy về các cập nhật quirck).
Fabricio Araujo

-2

Chỉ là một cách khác, tốn kém, nhưng phiên bản độc lập. Nó không sử dụng bảng tạm thời hoặc các biến.

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
Điều đó không tốt, điều đó rất chậm. Ngay cả khi bạn chỉ có 100 hàng, nó sẽ thực hiện đọc bóng bàn giữa các bảng ở mức 5.050 lần. 200 hàng, gấp 20.100 lần. Chỉ với 1.000 hàng, nó nhảy theo cấp số nhân lên tới 500.500 lượt đọc sqlblog.com/bloss/adam_machanic/archive/2006/07/12/ Lỗi
Michael Buen

Tôi thấy liên kết đến blog của bạn sau khi đăng bài này, bây giờ tôi thấy đây là một ý tưởng rất tồi, cảm ơn!
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.