SQL集合運算參考及案例(一):列值分組累計求和


概述

目前企業應用系統使用的大多數據庫都是關系型數據庫,關系數據庫依賴的理論就是針對集合運算的關系代數。關系代數是一種抽象的查詢語言,是關系數據操縱語言的一種傳統表達方式。不過我們在工作中發現,很多人在面對復雜的數據庫運算邏輯時會采用游標、循環、自定義函數等方式處理,因為游標是一種比較熟悉和舒適的面向過程的編程方式,很符合我們一般的邏輯思維習慣,可很不幸,這會導致糟糕的性能。顯然,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;
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM