SQL Server 开窗函数 Over()代替游标的使用详解
作者:为之守望 发布时间:2024-01-25 00:35:40
前言:
今天在优化工作中遇到的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,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
2、以往的游标写法
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数据表,为了呈现我们最终需要的数据样式,插入客户空行、期初余额、单据信息、客户小计等,逐行计算期末余额值的情况,最终效果如下:
3、使用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
代码说明:相比第二种,去除了游标的写法,通过了
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() 计算客户的订单号
SELECT DISTINCT FCustId,COUNT(FBillNo) OVER(PARTITION BY FCustId) FBillNum FROM DetailData
总结:
1、游标的使用场景可以很广,但是在数据量大的时候,就会显得很慢,一行一行遍历的速度还是挺久的
2、使用开窗函数来实现一些功能,还是很方便能实现效果,并且它的速度也是很快,值得推荐。
来源:https://www.cnblogs.com/cxt618/archive/2020/10/16/13826000.html


猜你喜欢
- 我就废话不多说了,直接上代码吧!# -*- coding: utf-8 -*-"""Created on Th
- 矩阵创建1、from numpyimport *;a1=array([1,2,3])a2=mat(a1)矩阵与方块列表的区别如下:2、dat
- JSON 基础简单地说,JSON 可以将 JavaScript 对象中表示的一组数据转换为字符串,然后就可以在函数之间轻松地传递这个字符串,
- 像这种不能创建一个.frm 文件的报错好像暗示着操作系统的文件的权限错误或者其它原因,但实际上,这些都不是的,事实上,这个mysql报错已经
- 前言shape函数是Numpy中的函数,它的功能是读取矩阵的长度,比如shape[0]就是读取矩阵第一维度的长度。直接用.shape可以快速
- 前言我第一次见到飞机大战是在我小学五年级下半学期的时候(2020年),这个游戏中可以说包含了几乎所有我目前可接触到的pygame知识。一、p
- 问题最近在工作中发现了一个问题,Python代码一直用着免费的Google翻译API插件googletrans,这两天突然就报错了:Trac
- 前言在前两篇文章中都使用HttpRequest这个http包来做api的请求然后github上面还有一个更有名,星星更多,社区也更活跃的ht
- 目录WSGI基本原理1. WSGI处理过程2. WSGI示例3. WSGI web服务器和应用程序WSGI基本原理1. WSGI处理过程浏览
- 两大类索引使用的存储引擎:MySQL5.7 InnoDB聚簇索引* 如果表设置了主键,则主键就是聚簇索引* 如果表没有主键,则会默认第一个N
- 我就废话不多说了,大家还是直接看代码吧!import tensorflow as tfimport numpy as npinput = t
- 在MAC/LINUX环境下,执行vi hello.py命令,并输入以下代码import webimport sysurls = ("
- 首先,我想说一下Javascript的装载和执行。通常来说,浏览器对于Javascript的运行有两大特性:1)载入后马上执行,2)执行时会
- 原图矫正后我新建了个jz的文件夹放相机矫正所需要拍摄的图片,如下:共12张 # coding:utf-8import cv2imp
- 在传统的递归中,典型的模式是,你执行第一个递归调用,然后接着调用下一个递归来计算结果。这种方式中途你是得不到计算结果,知道所有的递归调用都返
- 一、需要的参数1、通讯用户:touser 或 通讯组:toparty2、企业ID:corpid3、应用ID/密钥:agentId,secre
- 代码如下:'返回指定文件夹中文件的数目,传入值为被检测文件夹的硬盘绝对路径 function CountFile
- 用python读取excel表中的数据假如说有如下一张存储了数据的excel表,其中x1-x6是特征,y_label是特征对应的类别标签。我
- 前阵子刚完成一个B/S架构的学校办公系统,体会就是表太多,文件太多,而每个文件中类似的操作(代码)也太多了,例如学生信息和教师信息操作,st
- 由于marquee标签现在用得是越来越少了,所以滚动效果的做法大多也都改用javascript来实现了,至于不明白为什么不直接用marque