SQL Server 開窗函數 Over()代替遊標的使用

前言:今天在優化工做中遇到的sql慢的問題,發現之前用了挺多遊標來處理數據,這樣就致使在數據量多的狀況下,須要一行一行去遍歷從而計算須要的數據,這樣處理的結果就是數據慢,容易卡死。sql

語法介紹:

一、與Row_Number() 函數結合使用,對結果進行排序,這個是咱們使用的很是多的函數

SELECT ROW_NUMBER() OVER(ORDER BY FItemID DESC) FSort,* FROM Organization

  

二、與聚合函數結合使用,利用over子句的分組和排序,對須要的數據進行操做優化

例如:SUM() Over() 累加值、AVG() Over() 平均數
           MAX() Over() 最大值、MIN() Over() 最小值spa

具體介紹:

下面模擬工做中經過開窗函數代替遊標的例子,經過期初餘額與單據的預收金額、應收金額、實收金額來計算截止本單的期末餘額,在以往就是經過遊標一行一行去遍歷,計算須要的期末餘額,如今使用SUM() Over()來代替,最終要實現的效果圖以下:3d

第一行表示標題;第二行表示客戶,是一行空行;第三行是期初餘額,只顯示期末餘額的數據,第四至第六行表示的是每種單據的餘額狀況,並逐步彙總當前行的期末餘額數據;最後一行表示的是對客戶的合計。blog

一、構建須要用到的表和數據(簡略版)

--客戶表
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,1),
	FCustId			INT NOT NULL,
	FPreAmount		DECIMAL(28,10) NOT NULL DEFAULT(0),		--預收金額
	FReceivableAmount	DECIMAL(28,10) NOT NULL DEFAULT(0),		--應收金額
	FReceiveAmount		DECIMAL(28,10) NOT NULL DEFAULT(0)		--實收金額
)

--單據明細表
CREATE TABLE DetailData(
	FID			INT NOT NULL PRIMARY KEY IDENTITY(1,1),
	FCustId			INT NOT NULL,
	FDate			DATETIME NOT NULL,
	FBillType		NVARCHAR(64) NOT NULL,
	FBillNo			NVARCHAR(64) NOT NULL,
	FPreAmount		DECIMAL(28,10) NOT NULL DEFAULT(0),		--預收金額
	FReceivableAmount	DECIMAL(28,10) NOT NULL DEFAULT(0),		--應收金額
	FReceiveAmount		DECIMAL(28,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,0,0)
INSERT INTO InitialData(FCustId,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(2,8000,7245,0)
INSERT INTO InitialData(FCustId,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(3,0,1068.21,1068.00)
 
INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(1,'2020-06-30','委託結算','XSD20200700008',0,1221.56,0)
INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(1,'2020-06-30','委託結算','XSD20200700009',0,373.46,0)
INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(1,'2020-06-30','委託結算退貨','XSD20200700010',0,-427.05,0)
INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(1,'2020-07-30','銷售商品返利','XSFL20200700005',0,-17.9,0)

INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(2,'2020-06-25','預收退款','SKD20200700002',-755,0,0)
INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(2,'2020-06-20','銷售發貨','XSD20200700006',0,6169.50,6169.50)
INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(2,'2020-07-30','銷售總額返利','XSFL20200700002',0,-493.56,-421.85)
INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(2,'2020-07-31','其餘應收','QTYS20200900001',0,6000.00,0)
INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(2,'2020-06-20','預收衝應收','HXD20200700006',-7245.00,0,7245.00)

INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(3,'2020-06-30','銷售收款','SKD20200700003',0,0,2386.96)
INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(3,'2020-06-30','應收轉應收','HXD20200700007',0,2386.75,0)
INSERT INTO DetailData(FCustId,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount)
VALUES(3,'2020-07-08','銷售退貨','XSD20200700014',0,-46.80,0)
GO

二、以往的遊標寫法

SET NOCOUNT ON
--創建臨時表處理獲取數據
CREATE TABLE #DATA(
	FID			INT NOT NULL PRIMARY KEY IDENTITY(1,1),
	FClassTypeId		INT NOT NULL,
	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),		--預收金額
	FReceivableAmount	DECIMAL(28,10) NOT NULL DEFAULT(0),		--應收金額
	FReceiveAmount		DECIMAL(28,10) NOT NULL DEFAULT(0),		--實收金額
	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,FPreAmount,FReceivableAmount,FReceiveAmount
    From DetailData
    Order By FCustId,FDate,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,FBillType,FCustId,FNumber,FName,FPreAmount,FReceivableAmount,FReceiveAmount,FBalanceAmount) 
			SELECT -9999,FName + '小計',FItemID,FNumber,FName,@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,FBillType,FCustId,FNumber,FName) 
		SELECT -1000,FName,FItemID,FNumber,FName
		FROM Organization 
		WHERE FItemID = @CustId

		--獲取期初的期末餘額
		SELECT @LastAmount=isnull(FReceivableAmount,0) - isnull(FPreAmount,0) - isnull(FReceiveAmount,0),@PreAmount=isnull(FPreAmount,0),@ReceivableAmount=isnull(FReceivableAmount,0),@ReceiveAmount=isnull(FReceiveAmount,0) 
		FROM InitialData
		WHERE FCustId = @CustId

		INSERT INTO #DATA(FClassTypeId,FBillType,FCustId,FNumber,FName,FBalanceAmount) 
		VALUES(-1000,'期初餘額',@CustId,'','',@LastAmount)

		SELECT @Count = 1
		SELECT @SumBalanceAmount = @LastAmount
    END  

	--插入單據明細
	INSERT INTO #DATA(FClassTypeId,FCustId,FNumber,FName,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount,FBalanceAmount) 
	SELECT 0,d.FCustId,o.FNumber,o.FName,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount,@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,@CustId,@PreAmount,@ReceivableAmount,@ReceiveAmount
END 
IF @Count > 0 
BEGIN
	INSERT INTO #DATA(FClassTypeId,FBillType,FCustId,FNumber,FName,FPreAmount,FReceivableAmount,FReceiveAmount,FBalanceAmount) 
	SELECT -9999,FName + '小計',FItemID,FNumber,FName,@SumPreAmount,@SumReceivableAmount,@SumReceiveAmount,@LastAmount
	FROM Organization 
	WHERE FItemID = @OldCustId
	Select @SumPreAmount=0,@SumReceivableAmount=0,@SumReceiveAmount=0,@SumBalanceAmount=0,@LastAmount=0
END
CLOSE Data_cursor
DEALLOCATE Data_cursor

SELECT * FROM #DATA
ORDER BY FCustId,FID

DROP TABLE #DATA

代碼說明:建立了一個臨時表,使用遊標遍歷咱們的DetailData數據表,爲了呈現咱們最終須要的數據樣式,插入客戶空行、期初餘額、單據信息、客戶小計等,逐行計算期末餘額值的狀況,最終效果以下:排序

三、使用SUM() Over()的寫法

SET NOCOUNT ON
--創建臨時表處理獲取數據
CREATE TABLE #DATA(
	FID					INT NOT NULL PRIMARY KEY IDENTITY(1,1),
	FClassTypeId		INT NOT NULL,
	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),		--預收金額
	FReceivableAmount	DECIMAL(28,10) NOT NULL DEFAULT(0),		--應收金額
	FReceiveAmount		DECIMAL(28,10) NOT NULL DEFAULT(0),		--實收金額
	FBalanceAmount		DECIMAL(28,10) NOT NULL DEFAULT(0)      --期末餘額
)

--插入空行
INSERT INTO #DATA(FClassTypeId,FBillType,FCustId,FNumber,FName) 
SELECT -1000,FName,FItemID,FNumber,FName
FROM Organization o
INNER JOIN (SELECT FCustId FROM DetailData GROUP BY FCustId) d ON d.FCustId = o.FItemID

--插入期初餘額
INSERT INTO #DATA(FClassTypeId,FBillType,FCustId,FNumber,FName,FBalanceAmount) 
SELECT -1000,'期初餘額',FItemID,'','',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,FCustId,FNumber,FName,FDate,FBillType,FBillNo,FPreAmount,FReceivableAmount,FReceiveAmount,FBalanceAmount) 
SELECT 0,d.FCustId,o.FNumber,o.FName,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.FDate,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.FDate,d.FID

--插入小計
INSERT INTO #DATA(FClassTypeId,FBillType,FCustId,FNumber,FName,FPreAmount,FReceivableAmount,FReceiveAmount,FBalanceAmount) 
SELECT -9999,FName + '小計',d.FCustId,FNumber,FName,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.FName,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

代碼說明:相比第二種,去除了遊標的寫法,經過了ci

SUM(d.FReceivableAmount - d.FPreAmount - d.FReceiveAmount) OVER(PARTITION BY d.FCustId ORDER BY d.FCustId,d.FDate,d.FID)

來計算咱們須要的值,這個語法說明一下,sum是累加計算,計算應收金額 - 預收金額 - 實收金額(第二行計算出來的結果要加上第一行計算出來的結果,第三行計算出來的結果要加上第二行計算出來的結果,依次類推,因此,其餘聚合函數也是這種用法哦),PARTITION BY分組統計客戶,並經過Order by指定排序
這個PARTITION BY和Order By結果的用法就很關鍵了,否則計算就不是預期想要的
再舉個例子:好比使用Count() Over() 計算客戶的訂單號it

SELECT DISTINCT FCustId,COUNT(FBillNo) OVER(PARTITION BY FCustId) FBillNum FROM DetailData

總結:

一、遊標的使用場景能夠很廣,可是在數據量大的時候,就會顯得很慢,一行一行遍歷的速度仍是挺久的io

二、使用開窗函數來實現一些功能,仍是很方便能實現效果,而且它的速度也是很快,值得推薦。

相關文章
相關標籤/搜索