นี่คือวิธีการบางอย่างที่คุณสามารถเปรียบเทียบได้ ก่อนอื่นมาตั้งค่าตารางที่มีข้อมูลหุ่นจำลอง ฉันเติมข้อมูลนี้ด้วยข้อมูลสุ่มจาก 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 ผล:
ข้อสรุป
ในการทดสอบของฉันทางเลือกคือ:
- วิธี SQL Server 2012 ถ้าฉันมี SQL Server 2012
- ถ้า SQL Server 2012 ไม่พร้อมใช้งานและวันที่ของฉันต่อเนื่องกันฉันจะใช้วิธีเรียกซ้ำพร้อมกับวันที่
- หากไม่มีทั้ง 1. และ 2. ฉันจะไปด้วยตัวเองเข้าร่วมในการปรับปรุงที่เล่นโวหารถึงแม้ว่าประสิทธิภาพการทำงานจะปิดเพียงเพราะพฤติกรรมเป็นเอกสารและรับประกัน ฉันกังวลน้อยลงเกี่ยวกับความเข้ากันได้ในอนาคตเพราะหวังว่าหากการอัปเดตที่ผิดเพี้ยนเกิดขึ้นหลังจากฉันได้แปลงรหัสทั้งหมดเป็น 1 :-) แล้ว
แต่อีกครั้งคุณควรทดสอบสิ่งเหล่านี้กับสคีมาและข้อมูลของคุณ เนื่องจากนี่เป็นการทดสอบที่ประดิษฐ์ด้วยการนับแถวที่ค่อนข้างต่ำจึงอาจเป็นผายลมในสายลม ฉันได้ทำการทดสอบอื่นที่มีสคีมาและการนับแถวแตกต่างกันแล้วและฮิวริสติกของประสิทธิภาพแตกต่างกันมาก ... ซึ่งเป็นสาเหตุที่ฉันถามคำถามติดตามจำนวนมากถึงคำถามดั้งเดิมของคุณ
UPDATE
ฉัน blogged เพิ่มเติมเกี่ยวกับเรื่องนี้ที่นี่:
แนวทางที่ดีที่สุดสำหรับการเรียกใช้ผลรวม - อัปเดตสำหรับ SQL Server 2012
Day
เป็นกุญแจสำคัญและค่าที่อยู่ติดกัน?