概述
目前企業應用系統使用的大多數據庫都是關系型數據庫,關系數據庫依賴的理論就是針對集合運算的關系代數。關系代數是一種抽象的查詢語言,是關系數據操縱語言的一種傳統表達方式。不過我們在工作中發現,很多人在面對復雜的數據庫運算邏輯時會采用游標、循環、自定義函數等方式處理,因為游標是一種比較熟悉和舒適的面向過程的編程方式,很符合我們一般的邏輯思維習慣,可很不幸,這會導致糟糕的性能。顯然,SQL的總體目的是你要實現什么,而不是怎樣實現。大道至簡,我們在工作與學習的過程中經常會發現,更好的解決方案往往是簡單的,是高效的,是優雅的。
本人曾經用T-SQL重寫了一個基於游標的存儲過程,那個表只有100,000條記錄,原來的存儲過程用了40分鍾才執行完畢,而新的存儲過程只用了不到1秒。在這里,我想將自己遇到和收集到的關於集合運算與游標操作的對比展現給大家,以供參考。
問題描述
我們有時會遇到這樣一個問題,類似於某一列的值累計求和(即本條記錄的某個值=前幾列該值的合計)。我將解決的核心部分抽取出來。
--- 原始數據如下:
OID |
Period |
Amount |
Balance |
1 |
2009 |
3500.00 |
0.00 |
2 |
2009 |
5100.00 |
0.00 |
3 |
2009 |
10000.00 |
0.00 |
4 |
2010 |
2560.00 |
0.00 |
5 |
2010 |
4700.00 |
0.00 |
-- 預期結果如下(求Balance的值):
OID |
Period |
Amount |
Balance |
1 |
2009 |
3500.00 |
3500.00 |
2 |
2009 |
5100.00 |
8600.00 |
3 |
2009 |
10000.00 |
18600.00 |
4 |
2010 |
2560.00 |
2560.00 |
5 |
2010 |
4700.00 |
7260.00 |
創建測試數據的SQL腳本
CREATE TABLE tPeriod ( OID INT IDENTITY PRIMARY KEY , Period NVARCHAR(20) , Amount DECIMAL(18, 2) DEFAULT 0 , Balance DECIMAL(18, 2) DEFAULT 0 , Balance2 DECIMAL(18, 2) DEFAULT 0 , Balance3 DECIMAL(18, 2) DEFAULT 0 ) GO DECLARE @i INT SET @i = 1900 WHILE @i <= 2013 BEGIN INSERT INTO tPeriod(Period, Amount) SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) SET @i = @i + 1 END INSERT INTO tPeriod(Period, Amount) SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) UNION ALL SELECT CAST(@i AS NVARCHAR), ROUND(RAND() * 10000, -2) GO SELECT * FROM tPeriod; GO
傳統解答:使用游標
DECLARE @OID INT , @vPeriod_Pre NVARCHAR(20) , @vPeriod_Current NVARCHAR(20) , @dcAmount DECIMAL(18, 2) , @dcBalance DECIMAL(18, 2) DECLARE cursor1 CURSOR FOR SELECT t.OID, t.Period, t.Amount from tPeriod AS t OPEN cursor1 FETCH NEXT FROM cursor1 INTO @OID, @vPeriod_Current, @dcAmount SELECT @vPeriod_Pre = @vPeriod_Current, @dcBalance = 0 WHILE @@FETCH_STATUS = 0 BEGIN IF @vPeriod_Current = @vPeriod_Pre BEGIN SET @dcBalance = @dcBalance + @dcAmount END ELSE BEGIN SELECT @vPeriod_Pre = @vPeriod_Current, @dcBalance = @dcAmount END UPDATE tPeriod SET Balance = @dcBalance WHERE OID = @OID FETCH NEXT FROM cursor1 INTO @OID, @vPeriod_Current, @dcAmount END CLOSE cursor1 DEALLOCATE cursor1
推薦解答:集合運算
-- 參考答案2 UPDATE tPeriod SET Balance3 = ( SELECT SUM(Amount) FROM tPeriod AS t WHERE t.Period = tPeriod.Period AND t.OID <= tPeriod.OID ) GO -- 參考答案3(SQLSERVER) DECLARE @dcAmt DECIMAL(18, 2), @period CHAR(4) UPDATE T1 SET @dcAmt = CASE WHEN Period = @period THEN @dcAmt + Amount ELSE Amount END, @Period = Period, Balance2 = @dcAmt FROM tPeriod AS T1 GO
-- 參考答案3(Oracle) SELECT t.*, sum(t.amount) over(partition BY t.Period order by t.OID) as acc FROM tPeriod t;