SQL Server 開窗函式 Over()代替遊標的使用詳解
前言:
今天在優化工作中遇到的sql慢的問題,發現以前用了挺多遊標來處理資料,這樣就導致在資料量多的情況下,需要一行一行去遍歷從而計算需要的資料,這樣處理的結果就是資料慢,容易卡死。
語法介紹:
1、與Row_Number() 函式結合使用,對結果進行排序,這個是我們使用的非常多的
2、與聚合函式結合使用,利用over子句的分組和排序,對需要的資料進行操作
例如:SUM() Over() 累加值、AVG() Over() 平均數
MAX() Over() 最大值、MIN() Over() 最小值
具體介紹:
下面模擬工作中通過開窗函式代替遊標的例子,通過期初餘額與單據的預收金額、應收金額、實收金額來計算截止本單的期末餘額,在以往就是通過遊標一行一行去遍歷,計算需要的期末餘額,現在使用SUM() Over()來代替,最終要實現的效果圖如下:
第一行表示標題;第二行表示客戶,是一行空行;第三行是期初餘額,只顯示期末餘額的資料,第四至第六行表示的是每種單據的餘額情況,並逐步彙總當前行的期末餘額資料;最後一行表示的是對客戶的合計。
1、構建需要用到的表和資料(簡略版)
--客戶表 CREATE TABLE Organization( FItemID INT NOT NULL PRIMARY KEY IDENTITY(1,1),FNumber NVARCHAR(255),FName NVARCHAR(255) ) --期初資料表 CREATE TABLE InitialData( FID INT NOT NULL PRIMARY KEY IDENTITY(1,FCustId INT NOT NULL,FPreAmount DECIMAL(28,10) NOT NULL DEFAULT(0),--預收金額 FReceivableAmount DECIMAL(28,--應收金額 FReceiveAmount DECIMAL(28,10) NOT NULL DEFAULT(0) --實收金額 ) --單據明細表 CREATE TABLE DetailData( FID INT NOT NULL PRIMARY KEY IDENTITY(1,FDate DATETIME NOT NULL,FBillType NVARCHAR(64) NOT NULL,FBillNo NVARCHAR(64) NOT NULL,10) NOT NULL DEFAULT(0) --實收金額 ) INSERT INTO Organization(FNumber,FName) VALUES('001','北京客戶') INSERT INTO Organization(FNumber,FName) VALUES('002','上海客戶') INSERT INTO Organization(FNumber,FName) VALUES('003','廣州客戶') INSERT INTO InitialData(FCustId,FPreAmount,FReceivableAmount,FReceiveAmount) VALUES(1,0) INSERT INTO InitialData(FCustId,FReceiveAmount) VALUES(2,8000,7245,FReceiveAmount) VALUES(3,1068.21,1068.00) INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,'2020-06-30','委託結算','XSD20200700008',1221.56,0) INSERT INTO DetailData(FCustId,'XSD20200700009',373.46,'委託結算退貨','XSD20200700010',-427.05,'2020-07-30','銷售商品返利','XSFL20200700005',-17.9,0) INSERT INTO DetailData(FCustId,'2020-06-25','預收退款','SKD20200700002',-755,'2020-06-20','銷售發貨','XSD20200700006',6169.50,6169.50) INSERT INTO DetailData(FCustId,'銷售總額返利','XSFL20200700002',-493.56,-421.85) INSERT INTO DetailData(FCustId,'2020-07-31','其他應收','QTYS20200900001',6000.00,'預收衝應收','HXD20200700006',-7245.00,7245.00) INSERT INTO DetailData(FCustId,'銷售收款','SKD20200700003',2386.96) INSERT INTO DetailData(FCustId,'應收轉應收','HXD20200700007',2386.75,'2020-07-08','銷售退貨','XSD20200700014',-46.80,0) GO
2、以往的遊標寫法
SET NOCOUNT ON --建立臨時表處理獲取資料 CREATE TABLE #DATA( FID INT NOT NULL PRIMARY KEY IDENTITY(1,FClassTypeId INT NOT NULL,FNumber NVARCHAR(255),FName NVARCHAR(255),FDate DATETIME NULL,FBillType NVARCHAR(64) NULL,FBillNo NVARCHAR(64) NULL,--實收金額 FBalanceAmount DECIMAL(28,10) NOT NULL DEFAULT(0) --期末餘額 ) Declare @Id INT Declare @CustId INT Declare @PreAmount decimal(28,10) Declare @ReceivableAmount decimal(28,10) Declare @ReceiveAmount decimal(28,10) Declare @OldCustId int Declare @Count int Declare @LastAmount decimal(28,10) Declare @SumPreAmount decimal(28,10) Declare @SumReceivableAmount decimal(28,10) Declare @SumReceiveAmount decimal(28,10) Declare @SumBalanceAmount decimal(28,10) --使用遊標 Declare Data_cursor Cursor For Select FID,FCustId,FReceiveAmount From DetailData Order By FCustId,FID OPEN Data_cursor FETCH NEXT FROM Data_Cursor INTO @Id,@CustId,@PreAmount,@ReceivableAmount,@ReceiveAmount SET @OldCustId = @CustId SET @Count = 0 SET @LastAmount = 0 SET @SumPreAmount = 0 SET @SumReceivableAmount = 0 SET @SumReceiveAmount = 0 SET @SumBalanceAmount = 0 WHILE @@FETCH_STATUS = 0 BEGIN IF @Count > 0 BEGIN IF @OldCustId <> @CustId BEGIN --表示客戶已經變了,要插入小計 SET @Count = 0 INSERT INTO #DATA(FClassTypeId,FNumber,FName,FReceiveAmount,FBalanceAmount) SELECT -9999,FName + '小計',FItemID,@SumPreAmount,@SumReceivableAmount,@SumReceiveAmount,@LastAmount FROM Organization WHERE FItemID = @OldCustId Select @SumPreAmount=0,@SumReceivableAmount=0,@SumReceiveAmount=0,@SumBalanceAmount=0,@LastAmount=0 END END IF @Count = 0 BEGIN Set @OldCustId=@CustId --插入一行空行 INSERT INTO #DATA(FClassTypeId,FName) SELECT -1000,FName FROM Organization WHERE FItemID = @CustId --獲取期初的期末餘額 SELECT @LastAmount=isnull(FReceivableAmount,0) - isnull(FPreAmount,0) - isnull(FReceiveAmount,0),@PreAmount=isnull(FPreAmount,@ReceivableAmount=isnull(FReceivableAmount,@ReceiveAmount=isnull(FReceiveAmount,0) FROM InitialData WHERE FCustId = @CustId INSERT INTO #DATA(FClassTypeId,FBalanceAmount) VALUES(-1000,'期初餘額','',@LastAmount) SELECT @Count = 1 SELECT @SumBalanceAmount = @LastAmount END --插入單據明細 INSERT INTO #DATA(FClassTypeId,FBalanceAmount) SELECT 0,d.FCustId,o.FNumber,o.FName,@LastAmount + FReceivableAmount - FPreAmount - FReceiveAmount FROM DetailData d INNER JOIN Organization o ON d.FCustId = o.FItemID WHERE d.FCustId = @CustId AND FID = @Id SELECT @LastAmount = @LastAmount + FReceivableAmount - FPreAmount - FReceiveAmount,@SumPreAmount=@SumPreAmount + FPreAmount,@SumReceivableAmount=@SumReceivableAmount + FReceivableAmount,@SumReceiveAmount=@SumReceiveAmount + FReceiveAmount FROM DetailData WHERE FCustId = @CustId AND FID = @Id FETCH NEXT FROM Data_cursor INTO @Id,@ReceiveAmount END IF @Count > 0 BEGIN INSERT INTO #DATA(FClassTypeId,FBalanceAmount) SELECT -9999,@LastAmount FROM Organization WHERE FItemID = @OldCustId Select @SumPreAmount=0,@LastAmount=0 END CLOSE Data_cursor DEALLOCATE Data_cursor SELECT * FROM #DATA ORDER BY FCustId,FID DROP TABLE #DATA
程式碼說明:建立了一個臨時表,使用遊標遍歷我們的DetailData資料表,為了呈現我們最終需要的資料樣式,插入客戶空行、期初餘額、單據資訊、客戶小計等,逐行計算期末餘額值的情況,最終效果如下:
3、使用SUM() Over()的寫法
SET NOCOUNT ON --建立臨時表處理獲取資料 CREATE TABLE #DATA( FID INT NOT NULL PRIMARY KEY IDENTITY(1,FCustId INT NOT NULL,FNumber NVARCHAR(255),FName NVARCHAR(255),FDate DATETIME NULL,FBillType NVARCHAR(64) NULL,FBillNo NVARCHAR(64) NULL,FPreAmount DECIMAL(28,10) NOT NULL DEFAULT(0) --期末餘額 ) --插入空行 INSERT INTO #DATA(FClassTypeId,FName) SELECT -1000,FName FROM Organization o INNER JOIN (SELECT FCustId FROM DetailData GROUP BY FCustId) d ON d.FCustId = o.FItemID --插入期初餘額 INSERT INTO #DATA(FClassTypeId,FBalanceAmount) SELECT -1000,i.FReceivableAmount - i.FPreAmount -i.FReceiveAmount FROM Organization o INNER JOIN InitialData i ON o.FItemID = i.FCustId INNER JOIN (SELECT FCustId FROM DetailData GROUP BY FCustId) d ON d.FCustId = o.FItemID --插入單據明細(關鍵程式碼SUM() Over() ) INSERT INTO #DATA(FClassTypeId,FBalanceAmount) SELECT 0,d.FDate,d.FBillType,d.FBillNo,d.FPreAmount,d.FReceivableAmount,d.FReceiveAmount,SUM(d.FReceivableAmount - d.FPreAmount - d.FReceiveAmount) OVER(PARTITION BY d.FCustId ORDER BY d.FCustId,d.FID) + i.FReceivableAmount - i.FPreAmount - i.FReceiveAmount FROM DetailData d WITH(NOLOCK) INNER JOIN Organization o WITH(NOLOCK) ON o.FItemID = d.FCustId INNER JOIN InitialData i WITH(NOLOCK) ON o.FItemID = i.FCustId ORDER BY d.FCustId,d.FID --插入小計 INSERT INTO #DATA(FClassTypeId,FBalanceAmount) SELECT -9999,SUM(FPreAmount),SUM(FReceivableAmount),SUM(FReceiveAmount),0 FROM dbo.DetailData d INNER JOIN dbo.Organization o ON d.FCustId = o.FItemID GROUP BY d.FCustId,o.FNumber --更新小計的期末餘額 UPDATE d SET d.FBalanceAmount = d.FReceivableAmount - d.FPreAmount - d.FReceiveAmount + i.FReceivableAmount - i.FPreAmount - i.FReceiveAmount FROM #DATA d INNER JOIN InitialData i ON d.FCustId = i.FCustId WHERE d.FClassTypeId = -9999 SELECT * FROM #DATA ORDER BY FCustId,FID DROP TABLE #DATA
程式碼說明:相比第二種,去除了遊標的寫法,通過了
SUM(d.FReceivableAmount - d.FPreAmount - d.FReceiveAmount) OVER(PARTITION BY d.FCustId ORDER BY d.FCustId,d.FID)
來計算我們需要的值,這個語法說明一下,sum是累加計算,計算應收金額 - 預收金額 - 實收金額(第二行計算出來的結果要加上第一行計算出來的結果,第三行計算出來的結果要加上第二行計算出來的結果,依次類推,所以,其他聚合函式也是這種用法哦),PARTITION BY分組統計客戶,並通過Order by指定排序
這個PARTITION BY和Order By結果的用法就很關鍵了,不然計算就不是預期想要的
再舉個例子:比如使用Count() Over() 計算客戶的訂單號
SELECT DISTINCT FCustId,COUNT(FBillNo) OVER(PARTITION BY FCustId) FBillNum FROM DetailData
總結:
1、遊標的使用場景可以很廣,但是在資料量大的時候,就會顯得很慢,一行一行遍歷的速度還是挺久的
2、使用開窗函式來實現一些功能,還是很方便能實現效果,並且它的速度也是很快,值得推薦。
到此這篇關於SQL Server 開窗函式 Over()代替遊標的使用的文章就介紹到這了,更多相關SQL Server 開窗函式 Over()內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!