[SQL][Performance]在 SQL Server 2005/2008 版本中處理累加的問題
最近在跟朋友的討論中,看到他有說明一個很特別的案例,要來處理資料集按照某些排列順序做出累加,在早期 SQL Server 的版本中,多半會使用 temp table 或者是 cursor 的方式來做處理,而到了 SQL Server 2005 版本之後,由於資料庫引擎開始支援 CTE ( Common Table Expression ) - 此部分可以參考小弟另外一篇文章的說明。因此如果遇到累加的時候,透過 CTE 的協助就可以方便許多了。在此我先做一個簡單的範例來說明
首先先利用以下的語法產生一個測試用的 Table
SET NOCOUNT ON
IF OBJECT_ID('Items') IS NOT NULL DROP TABLE Items
DECLARE @Items TABLE
(
Item_Id varchar(10), Item_Name varchar(10), Sales_Qty numeric(10)
)
DECLARE @Ptr INT, @RecordCount INT
SET @Ptr = 0
SET @RecordCount = 5000 -- 範例表的筆數
WHILE @Ptr < @RecordCount
BEGIN
SET @Ptr += 1
INSERT INTO @Items
VALUES ( RIGHT('000000000'+LTRIM(STR(@Ptr)),10 ), RIGHT(newid(),10 ) , ROUND( RAND()*10000,1 ) )
END
SELECT * INTO Items FROM @Items -- 產生測試表格 Items
因此在 SQL Server 2005 之後我們可以利用以下的方式來進行
WITH ItemsByRank AS
(
SELECT *,RANK() OVER ( ORDER BY Sales_Qty DESC, Item_Id) AS Sales_Rank FROM Items
)
SELECT A.*,SUM(B.Sales_Qty) Sales_Cumulative FROM ItemsByRank A
JOIN ItemsByRank B ON A.Sales_Rank >= B.Sales_Rank
GROUP BY A.Item_Id,A.Item_Name,A.Sales_Qty,A.Sales_Rank
ORDER BY A.Sales_Rank
雖然這個方式不如 SQL Server 2012 的 Window Function 來的簡潔,但至少在 SQL Server 2005/2008 都還算是可以接受的方式
-- SQL Server 2012 Window Function
SELECT *,
RANK() OVER ( ORDER BY Sales_Qty DESC) AS Sales_Rank,
SUM(Sales_Qty) OVER ( ORDER BY Sales_Qty DESC) AS Sales_Cumulative
FROM Items
但如果把時間也考量進去,那差異可就大了,在資料筆數少的時候還沒有太大的感覺,但如果資料筆數多的狀況下,那時間隨著資料筆數呈現平方倍數的增加
資料數 |
1,000 |
10,000 |
20,000 |
30,000 |
50,000 |
CTE |
1s |
25s |
79s |
117s |
45m |
Window |
1s |
1s |
1s |
1s |
1s |
而就這麼湊巧,這幾天就剛好遇到一個案件,他的實際資料大約有 90,000 筆資料,而這些資料必須要按照 2 種不同的欄位排序,並且針對排序的結果將商品銷售數量做累加,就算他已經用了非常高檔的設備,整個計算下來還是要花費將近 2hr 的時間,因此希望能在不要升級 SQL Server 的狀況下,找出解決的辦法。
IF OBJECT_ID('tempdb..#Items_with_rank') IS NOT NULL DROP TABLE #Items_with_rank
DECLARE @mock TABLE( rank_index numeric(10) primary key , amount numeric(21) )
-- 建立一個存放排序後的資料暫存 Table , 因為 Table 變數不支援 Index, 所以使用暫存 Table
CREATE TABLE #Items_with_rank( Item_Id varchar(10), Sales_Qty numeric(21), Sales_rank INT primary key );
CREATE INDEX X1 ON #Items_with_rank(Item_Id)
-- 產生按照銷售數量排序的暫存表
INSERT INTO #Items_with_rank
SELECT Item_Id, Sales_Qty, RANK() OVER ( ORDER BY Sales_Qty DESC,Item_Id ) FROM Items ;
declare @ptr numeric(10);
declare @max numeric(10);
select @max=count(*) from Items ;
-- 第一筆資料不用考慮累加
insert into @mock select Sales_rank,Sales_Qty from #Items_with_rank where Sales_rank = 1
set @ptr = 1;
while ( @ptr < @max )
begin
set @ptr = @ptr + 1
-- 利用前一筆累計的數量加上自己的數量,算出到自己的累計值
insert into @mock select Sales_rank,Sales_Qty+amount from #Items_with_rank join @mock on Sales_rank-1 =rank_index where Sales_rank = @ptr
end
-- 列出累加結果
select A.*,C.amount
from Items A
join #Items_with_rank B ON A.Item_Id = B.Item_Id
join @mock C on B.Sales_rank = C.rank_index
order by B.Sales_rank
當採用這樣的方式時,50,000 筆資料計算需要 15sec,雖然不如直接採用 Window Function 的 1sec ,但相對採用 CTE 需要 45mins 要來的好太多了。因此如果您有大量資料需要做到類似的處理時,又沒有辦法採用 SQL Server 2012 以上的版本,那麼可以試試看上述的範例,利用類似 Cursor 的方式來循序處理,這樣在大量資料時會比兩個大資料表相互 JOIN 在做計算的效果還要好上太多了。