2012-05-31 121 views
2

我正在尋找一種快速方式來創建由特定列進行分區的大型SQL Server 2008數據集中的累積總計,可能使用多個賦值變量解決方案。作爲一個非常簡單的例子,我想打造的「cumulative_total」下面列:在運行總計查詢中對分區進行分區

user_id | month | total | cumulative_total 

1  | 1  | 2.0 | 2.0 
1  | 2  | 1.0 | 3.0 
1  | 3  | 3.5 | 8.5 

2  | 1  | 0.5 | 0.5 
2  | 2  | 1.5 | 2.0 
2  | 3  | 2.0 | 4.0 

我們傳統上做到了這一點與相關子查詢,但在大量的數據(200,000行和幾個不同的類別的運行總數),這並不能給我們提供理想的性能。

我最近讀到有關使用多個變量賦值累積相加這裏:

http://sqlblog.com/blogs/paul_nielsen/archive/2007/12/06/cumulative-totals-screencast.aspx

在這個例子中該博客累計可變的解決方案是這樣的:

UPDATE my_table 
SET @[email protected]+ISNULL(total, 0) 

該解決方案似乎在上面的例子(用戶1或用戶2)中對於單個用戶的總結速度非常快。但是,我需要按用戶進行有效分區 - 按月向用戶提供累計總額。

有誰知道擴展多個賦值變量的概念來解決這個問題,或任何其他想法以外的相關子查詢或遊標嗎?

非常感謝您的任何提示。

+0

你有大量的用戶或大量個月或兩者?也是什麼版本的SQL Server? –

+0

嗨亞倫。大量的用戶,但只有幾個月(從未超過約24)。 SQL Server 2008. –

+0

我認爲你的第三行應該有'cumulative_total = 6.5',而不是'8.5'。 –

回答

2

你在SQL Server 2008中的選擇是合理的限制 - 你可以根據上面的方法做一些事情(稱爲'古怪的更新'),或者你可以在CLR中做一些事情。

就我個人而言,我會和CLR一起工作,因爲它可以保證工作,而古怪的更新語法不是正式支持的(所以可能會在未來版本中打破)。

你正在尋找的古怪更新語法的變化會是這樣的:

UPDATE my_table 
SET @CumulativeTotal=cumulative_total=ISNULL(total, 0) + 
     CASE WHEN @[email protected] THEN @CumulativeTotal ELSE 0 END, 
    @user=lastUser 

值得一提的是,在SQL Server 2012中引入了窗口函數RANGE支持,所以這是一種方式表達這是最高效的,同時得到100%的支持。

+0

非常好,那很完美 - 謝謝你Matt。對支持的關注以及關於RANGE的說明受到關注和讚賞。 –

+0

@Matt,正如我在回答結束時指出的那樣,如果你已經在使用'RANGE',你應該測試'ROWS'產生相同的結果(它並不是在所有情況下),因爲它*應該*更有效率。另外,您是否知道任何已發佈的CLR解決方案都比本頁面上的方法更快?我知道CLR對於某些事情來說太棒了(例如分割一個字符串),但是特別是在這個問題上更好嗎?數學計算稍快一點?我希望CLR的開銷超過了它的任何好處(但很高興被證明是錯誤的)。 –

+0

@AaronBertrand - 是的,我應該明確地說出RANGE/ROWS,因爲我傾向於將整個語法擴展看作一個屋檐下。我所說的CLR位是在一個線軸上進行計算,這很好。之前我在CLR上做過這樣的事情,結果非常好。嘗試並獲得一些好的測量值將是一個有趣的練習。 –

6

如果您不需要存儲數據(您不應該這樣做,因爲您需要在任何時候更改,添加或刪除任何行時更新運行總計),並且如果您不相信這個古怪的更新(你不應該這樣做,因爲它不能保證工作,並且它的行爲可能會隨着修補程序,服務包,升級甚至底層索引或統計信息的改變而改變),你可以在運行時嘗試這種類型的查詢。這是MVP Hugo Kornelis創建的「基於集合的迭代」的方法(他在他的章節SQL Server MVP Deep Dives中發佈了類似的東西)。由於運行總計通常需要在整個集合上有一個遊標,整個集合有一個古怪的更新,或者隨着行數增加,單個非線性自連接變得越來越昂貴,這裏的訣竅是循環一些有限的元素(在這種情況下,每個用戶每月的每行的「排名」,並且每個用戶/月份組合中的每個排名只處理一次,所以不是循環遍歷200,000行,你循環達24次)。

DECLARE @t TABLE 
(
    [user_id] INT, 
    [month] TINYINT, 
    total DECIMAL(10,1), 
    RunningTotal DECIMAL(10,1), 
    Rnk INT 
); 

INSERT @t SELECT [user_id], [month], total, total, 
    RANK() OVER (PARTITION BY [user_id] ORDER BY [month]) 
    FROM dbo.my_table; 

DECLARE @rnk INT = 1, @rc INT = 1; 

WHILE @rc > 0 
BEGIN 
    SET @rnk += 1; 

    UPDATE c SET RunningTotal = p.RunningTotal + c.total 
    FROM @t AS c INNER JOIN @t AS p 
    ON c.[user_id] = p.[user_id] 
    AND p.rnk = @rnk - 1 
    AND c.rnk = @rnk; 

    SET @rc = @@ROWCOUNT; 
END 

SELECT [user_id], [month], total, RunningTotal 
FROM @t 
ORDER BY [user_id], rnk; 

結果:

user_id month total RunningTotal 
------- ----- ----- ------------ 
1  1  2.0  2.0 
1  2  1.0  3.0 
1  3  3.5  6.5 -- I think your calculation is off 
2  1  0.5  0.5 
2  2  1.5  2.0 
2  3  2.0  4.0 

當然你從這個表變量可以更新基表,但何必呢,因爲這些存儲的值僅在下一次到表被感動好由任何DML語句?

UPDATE mt 
    SET cumulative_total = t.RunningTotal 
    FROM dbo.my_table AS mt 
    INNER JOIN @t AS t 
    ON mt.[user_id] = t.[user_id] 
    AND mt.[month] = t.[month]; 

由於我們不依賴於任何類型的隱含排序,這是100%的支持,相對於不支持的離奇更新的性能比較值得。即使它沒有擊敗它,但接近,你應該考慮使用它恕我直言。

對於SQL Server 2012的解決方案,馬特提到RANGE但由於此方法使用磁盤上的卷軸,你也應該與ROWS測試,而不是僅僅與RANGE運行。這裏是你的情況下,一個簡單的例子:

SELECT 
    [user_id], 
    [month], 
    total, 
    RunningTotal = SUM(total) OVER 
    (
    PARTITION BY [user_id] 
    ORDER BY [month] ROWS UNBOUNDED PRECEDING 
) 
FROM dbo.my_table 
ORDER BY [user_id], [month]; 

RANGE UNBOUNDED PRECEDING或沒有ROWS\RANGE(此時也將使用RANGE磁盤上的線軸)相比較。儘管計劃看起來稍微複雜一些(一個額外的序列項目操作員),但上述方法的總體持續時間更短,並且方法的I/O減少了

我最近發表的一篇博客文章中概述了一些性能上的差異我爲特定的運行總計場景觀察:

http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals

+0

謝謝Aaron,這非常有幫助。非常感激。 –