วิ่งรวมกับการนับ?


34

ตามชื่อเรื่องแนะนำว่าฉันต้องการความช่วยเหลือในการหาผลรวมสะสมใน T-SQL ปัญหาคือผลรวมที่ฉันต้องทำคือผลรวมของการนับ:

sum(count (distinct (customers))) 

พูดถ้าฉันวิ่งนับคนเดียวผลลัพธ์จะเป็น:

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

ฉันต้องการผลลัพธ์ที่มีผลรวมเป็น:

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

ฉันใช้ผลรวมทั้งหมดก่อนใช้coalesceวิธีการ แต่ไม่นับด้วย ฉันไม่แน่ใจว่าจะทำอย่างไรตอนนี้ที่ฉันมีการนับ


2
กรุณาใช้ SQL Server รุ่นใด คุณสามารถแบ่งปันขอบเขตของข้อมูล - เรากำลังพูดถึง 1,000 แถวล้านหนึ่งล้านหรือไม่ มันเป็นแค่สองคอลัมน์นี้จริง ๆ หรือคุณทำให้สคีมาของเราง่ายขึ้น? ในที่สุดก็Dayเป็นกุญแจสำคัญและค่าที่อยู่ติดกัน?
Aaron Bertrand

ฉันสร้างบล็อกที่ครอบคลุมเกี่ยวกับการรันทั้งหมด (การอัปเดตที่แปลกประหลาดเทียบกับ Hybrid Recursive CTE กับ Cursor): ienablemuch.com/2012/05/…ฉันไม่ได้รวมผลรวมการวิ่งที่ใช้วิธีการตั้งค่าแบบบริสุทธิ์ประสิทธิภาพไม่มีอะไรจะเป็น เป็นที่ต้องการ: sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen

คำตอบ:


53

นี่คือวิธีการบางอย่างที่คุณสามารถเปรียบเทียบได้ ก่อนอื่นมาตั้งค่าตารางที่มีข้อมูลหุ่นจำลอง ฉันเติมข้อมูลนี้ด้วยข้อมูลสุ่มจาก sys.all_columns มันเป็นการสุ่ม - ฉันรับรองว่าวันที่จะต่อเนื่องกัน (ซึ่งสำคัญมากสำหรับคำตอบข้อใดข้อหนึ่งเท่านั้น)

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;

ผล:

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

ข้อมูลมีลักษณะเช่นนี้ (5,000 แถว) - แต่จะมีลักษณะแตกต่างกันเล็กน้อยในระบบของคุณขึ้นอยู่กับรุ่นและบิลด์ #:

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

และผลลัพธ์รวมที่ทำงานควรมีลักษณะเช่นนี้ (501 แถว):

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

ดังนั้นวิธีที่ฉันจะเปรียบเทียบคือ:

  • "self-join" - วิธีพิถีพิถัน
  • "CTE แบบเรียกซ้ำพร้อมกับวันที่" - ขึ้นอยู่กับวันที่ต่อเนื่อง (ไม่มีช่องว่าง)
  • "CTE แบบเรียกซ้ำพร้อมกับ row_number" - คล้ายกับด้านบน แต่ช้ากว่าอาศัย ROW_NUMBER
  • "recursive CTE พร้อม #temp table" - ถูกขโมยจากคำตอบของ Mikael ตามที่แนะนำ
  • "การอัปเดตที่เล่นโวหาร" ซึ่งในขณะที่พฤติกรรมที่ไม่ได้รับการสนับสนุนและไม่ได้สัญญาว่าน่าจะได้รับความนิยม
  • "เคอร์เซอร์"
  • SQL Server 2012 โดยใช้ฟังก์ชันการทำงานของหน้าต่างใหม่

ตัวเองเข้าร่วม

นี่คือวิธีที่ผู้คนจะบอกให้คุณทำเมื่อพวกเขาเตือนคุณให้อยู่ห่างจากเคอร์เซอร์เพราะ "set-based เร็วกว่าเสมอ" ในการทดลองเมื่อเร็ว ๆ นี้บางส่วนฉันพบว่าเคอร์เซอร์อยู่นอกหน้าโซลูชันนี้

;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 แบบเรียกซ้ำพร้อมกับวันที่

การแจ้งเตือน - สิ่งนี้ขึ้นอยู่กับวันที่ต่อเนื่อง (ไม่มีช่องว่าง) การเรียกซ้ำสูงสุด 10,000 ระดับและคุณทราบวันที่เริ่มต้นของช่วงที่คุณสนใจ (เพื่อกำหนดจุดยึด) คุณสามารถกำหนดจุดยึดแบบไดนามิกโดยใช้แบบสอบถามย่อยแน่นอน แต่ฉันต้องการทำให้สิ่งต่าง ๆ ง่ายขึ้น

;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 แบบเรียกซ้ำพร้อมกับ row_number

การคำนวณ Row_number มีราคาแพงเล็กน้อย สิ่งนี้รองรับระดับการเรียกซ้ำสูงสุดที่ 10,000 แต่คุณไม่จำเป็นต้องกำหนดจุดยึด

;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 แบบเรียกซ้ำพร้อมกับตาราง temp

ขโมยจากคำตอบของ Mikael ตามที่แนะนำเพื่อรวมไว้ในการทดสอบ

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;

การปรับปรุงที่เล่นโวหาร

อีกครั้งฉันแค่รวมสิ่งนี้เพื่อความสมบูรณ์เท่านั้น ฉันเองจะไม่พึ่งพาโซลูชันนี้เนื่องจากฉันได้กล่าวถึงคำตอบอื่นวิธีนี้ไม่รับประกันว่าจะทำงานได้ทั้งหมดและอาจแตกใน SQL Server รุ่นในอนาคตอย่างสมบูรณ์ (ฉันพยายามอย่างเต็มที่ที่จะบีบบังคับ SQL Server ให้เชื่อฟังคำสั่งที่ฉันต้องการโดยใช้คำแนะนำสำหรับตัวเลือกดัชนี)

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;

เคอร์เซอร์

"ระวังมีเคอร์เซอร์อยู่ที่นี่! เคอร์เซอร์ชั่ว! คุณควรหลีกเลี่ยงเคอร์เซอร์ทั้งหมด!" ไม่นั่นไม่ใช่ฉันพูดถึงมันเป็นเพียงสิ่งที่ฉันได้ยินมาก ตรงกันข้ามกับความเห็นที่นิยมมีบางกรณีที่เคอร์เซอร์เหมาะสม

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 เวอร์ชันล่าสุดการปรับปรุงฟังก์ชันการทำงานของหน้าต่างช่วยให้เราสามารถคำนวณผลรวมการรันได้อย่างง่ายดายโดยไม่ต้องเสียค่าใช้จ่ายแบบทวีคูณในการเข้าร่วมตัวเอง (SUM ถูกคำนวณในหนึ่งรอบ) ความซับซ้อนของ CTEs (รวมถึงข้อกำหนด ของแถวที่ต่อเนื่องกันสำหรับ CTE ที่ทำงานได้ดีกว่า) การปรับปรุงที่ไม่สนับสนุนและเคอร์เซอร์ต้องห้าม เพียงแค่ระวังความแตกต่างระหว่างการใช้RANGEและROWSหรือไม่ระบุเลย - เพียงROWSหลีกเลี่ยงสปูลบนดิสก์ซึ่งจะขัดขวางประสิทธิภาพการทำงานอย่างมาก

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

การเปรียบเทียบประสิทธิภาพ

ฉันใช้วิธีแต่ละวิธีแล้วห่อเป็นชุดโดยใช้วิธีต่อไปนี้:

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

นี่คือผลลัพธ์ของระยะเวลาทั้งหมดในหน่วยมิลลิวินาที (จำได้ว่ารวมคำสั่ง 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

และฉันทำมันอีกครั้งโดยไม่ใช้คำสั่ง 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

การลบทั้ง DBCC และลูปเพียงวัดซ้ำหนึ่งซ้ำ:

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

ในที่สุดฉันก็คูณจำนวนแถวในตารางต้นฉบับด้วย 10 (เปลี่ยนด้านบนเป็น 50,000 และเพิ่มตารางอื่นเป็นการรวมแบบไขว้) ผลลัพธ์ของสิ่งนี้การวนซ้ำหนึ่งครั้งโดยไม่มีคำสั่ง DBCC (เพียงเพื่อผลประโยชน์ของเวลา):

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

ฉันวัดระยะเวลา - ฉันจะปล่อยให้มันเป็นแบบฝึกหัดกับผู้อ่านเพื่อเปรียบเทียบวิธีการเหล่านี้กับข้อมูลของพวกเขาเปรียบเทียบตัวชี้วัดอื่น ๆ ที่อาจมีความสำคัญ (หรืออาจแตกต่างกับสคีมา / ข้อมูลของพวกเขา) ก่อนที่จะสรุปข้อสรุปใด ๆ จากคำตอบนี้จะขึ้นอยู่กับคุณที่จะทดสอบกับข้อมูลของคุณและสคีมาของคุณ ... ผลลัพธ์เหล่านี้จะเปลี่ยนไปเมื่อการนับแถวสูงขึ้น


การสาธิต

ฉันได้เพิ่ม sqlfiddle ผล:

ป้อนคำอธิบายรูปภาพที่นี่


ข้อสรุป

ในการทดสอบของฉันทางเลือกคือ:

  1. วิธี SQL Server 2012 ถ้าฉันมี SQL Server 2012
  2. ถ้า SQL Server 2012 ไม่พร้อมใช้งานและวันที่ของฉันต่อเนื่องกันฉันจะใช้วิธีเรียกซ้ำพร้อมกับวันที่
  3. หากไม่มีทั้ง 1. และ 2. ฉันจะไปด้วยตัวเองเข้าร่วมในการปรับปรุงที่เล่นโวหารถึงแม้ว่าประสิทธิภาพการทำงานจะปิดเพียงเพราะพฤติกรรมเป็นเอกสารและรับประกัน ฉันกังวลน้อยลงเกี่ยวกับความเข้ากันได้ในอนาคตเพราะหวังว่าหากการอัปเดตที่ผิดเพี้ยนเกิดขึ้นหลังจากฉันได้แปลงรหัสทั้งหมดเป็น 1 :-) แล้ว

แต่อีกครั้งคุณควรทดสอบสิ่งเหล่านี้กับสคีมาและข้อมูลของคุณ เนื่องจากนี่เป็นการทดสอบที่ประดิษฐ์ด้วยการนับแถวที่ค่อนข้างต่ำจึงอาจเป็นผายลมในสายลม ฉันได้ทำการทดสอบอื่นที่มีสคีมาและการนับแถวแตกต่างกันแล้วและฮิวริสติกของประสิทธิภาพแตกต่างกันมาก ... ซึ่งเป็นสาเหตุที่ฉันถามคำถามติดตามจำนวนมากถึงคำถามดั้งเดิมของคุณ


UPDATE

ฉัน blogged เพิ่มเติมเกี่ยวกับเรื่องนี้ที่นี่:

แนวทางที่ดีที่สุดสำหรับการเรียกใช้ผลรวม - อัปเดตสำหรับ SQL Server 2012


1

เห็นได้ชัดว่านี่คือทางออกที่ดีที่สุด

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

ความคิดใด ๆ ที่ไม่มีการใช้ตารางชั่วคราว (proc ของฉันกำลังบังคับค่าผ่านหลายตารางชั่วคราวตามความจำเป็นฉันจึงพยายามหาวิธีหลีกเลี่ยงการใช้ตารางชั่วคราวอื่น) ถ้าไม่ฉันจะใช้วิธีนี้ ฉันคิดว่ามันจะใช้งานได้

นอกจากนี้ยังสามารถทำได้ด้วยการเข้าร่วมด้วยตนเองหรือแบบสอบถามย่อยที่ซ้อนกัน แต่ตัวเลือกเหล่านี้ทำงานได้ไม่ดีนัก นอกจากนี้ยังเป็นไปได้ว่าคุณจะกดปุ่ม tempdb ด้วยตัวเลือกเหล่านี้ด้วยการสปูลหรือเวิร์คตาราง

3
เพิ่งทราบว่าวิธีการ "เล่นโวหารปรับปรุง" นี้ไม่รับประกันว่าจะทำงาน - ไวยากรณ์นี้ไม่ได้รับการสนับสนุนและพฤติกรรมของมันไม่ได้กำหนดและมันสามารถทำลายในรุ่นอนาคตแก้ไขด่วนหรือเซอร์วิสแพ็ค ดังนั้นในขณะที่ใช่มันเร็วกว่าทางเลือกที่รองรับบางอย่าง แต่อาจมีค่าใช้จ่ายในการทำงานร่วมกันในอนาคต
Aaron Bertrand

6
มีคำเตือนมากมายสำหรับแนวทางนี้ซึ่ง Jeff Moden ได้เขียนไว้ที่ใดที่หนึ่ง คุณควรมีดัชนีคลัสเตอร์ในdayตัวอย่างเช่น
Martin Smith

2
@MartinSmith มันเป็นบทความที่ใหญ่มากที่ sqlservercentral.com (ไปที่หน้าผู้เขียนและค้นหาบทความของเขาเกี่ยวกับการปรับปรุง quirck)
Fabricio Araujo

-2

อีกวิธีหนึ่งราคาแพง แต่เป็นเวอร์ชั่นที่เป็นอิสระ มันไม่ได้ใช้ตารางชั่วคราวหรือตัวแปร

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
นั่นไม่ดีนั่นช้ามาก แม้คุณจะมี 100 แถวก็จะอ่านปิงปองระหว่างตารางที่ 5,050 ครั้ง 200 แถวคือ 20,100 ครั้ง ด้วย 1,000 แถวเท่านั้นมันจะเพิ่มขึ้นเป็น 500,500 อ่านsqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen

ฉันเห็นลิงก์ไปยังบล็อกของคุณหลังจากโพสต์ข้อความนี้ตอนนี้ฉันเห็นว่ามันเป็นความคิดที่แย่มากขอบคุณ!
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.