-- 第一章 SQL窗口函数
/*
窗口函数的作用域是由OVER 子句定义的数据行集合。其概念的精髓在于可以通过对数据行集合
或对数据行窗口进行多种计算(汇总,移动平均值,找数据岛),最后的得到单个值
*/
1.1 窗口函数的背景
1.1.1 窗口函数的描述
USE TSQL2012;
SELECT orderid, orderdate, val,
rank() OVER (ORDER BY val DESC) as rnk
FROM Sales.OrderValues
ORDER BY rnk;
-- 这里有个问题就是究竟什么叫窗口?伏笔
/*
窗口函数有四种类型:集合,排序,分布,和偏移。
聚合函数是诸如:SUM,COUNT,MIN,MAX,(之前在GROUP BY 里面用过)一个聚合函数的作用域是一个记录集,
这个记录集由一个分组查询或一个窗口描述来定义。
剩下三种用到了再说。
下面这本书将教我完成:
1.分页
2.去除重复数据
3.返回每组前n条记录
4.计算累积合计
5.对时间间隔进行操作,统计最大的并发会话数
6.找出数据差距(gap)和数据岛(island)
7.计算百分比
8.计算分布的模式
9.排序层次结构
10.数据透视
11.计算时效性(计算近因)
声明性语言(SQL)和优化
在SQL中,我们仅仅从逻辑上声明我们的需求,而不是具体描述如何实现它。为什么目的一致,形式不同的两种需求
如一个使用窗口函数,另一个不用将是不同的性能?SQL不能分辨这两种不同的形式代表同一个意思,从而为两种形式
解析出同一种查询执行。
*/
1.1.2 基于集 合与基于迭代/游标的编程
--什么是游标?
游标(CURSOR)是一个存储在sql服务器上得到数据库查询,它不是一条SELECT语句,而是被该语句检索出来的结果集。
在存储了游标之后,应用程序可以根据需要滚动或浏览其中的数据。
在《mysql必知必会》这本书上有一个关于游标的例子:循环检索数据,从第一行到最后一行。
CREATE PROCEDURE processorders()
BEGIN
-- DECLARE local variables
DECLARE done boolean DEFAULT 0;
DECLARE o INT;
DECLARE t DECIMAL(8,2);
-- DECLARE the cursor
DECLARE ordernumbers CURSOR
FOR
SELECT order_num FROM orders;
-- DECLARE continue handler
DECLARE countinue handler FOR SQLSTATE '02000' SET done = 1;
--CREATE TABLE TO store the results
CREATE TABLE IF NOT EXISTS ordertotals
(order_num INT, total DECIMAL(8,2));
-- OPEN the cursor
OPEN ordernumbers;
--loop all rows
repeat
FETCH ordernumbers into o;
call ordertotal(o, 1, t);
INSERT INTO ordertotals(order_num, total)
VALUES(o, t);
until done END repeat;
CLOSE ordernumbers;
END;
T-SQL的查询任务的解决方案要么基于集合,要么基于迭代/游标。
集合的定义和内涵很丰富,重点在于两个,整体和顺序。
整体:一个集合应该被当做一个整体来被感知和操作,我们的关注点应该放在集合这个整体上,而不是其中的各个元素。
迭代操作则正好相反,因为文件中的记录或游标都是逐个处理的。当用基于集合的查询与数据库中的表进行交互时,我们是与
整个表交互,而不是与表中的单个记录进行交互——这个思维一上来很难接纳。
顺序:集合中没有对元素顺序进行规定。文件和游标中的记录都有特定的顺序,每次提取一条记录时,我们知道它们的提取就是
按照和这个顺序,表中的行是没有顺序的,因为一张表就是一个集合。不要混淆 数据模型的逻辑表达和物理层面的实现语言。
编写SQL查询,我们的思维要基于集合的概念进行,这也就是为什么窗口函数可以基于迭代的思维(每次一条,以确定顺序)和
基于集合的思维(集合是一个整体,没有顺序)之间架起桥梁,帮助我们从一种思维模式转换到另一种思维模式,这是窗口函数的独创思维。
注意,函数支持顺序限定,并时不时说明它违反了关系型概念,查询的输入是关系型,对于没有顺序的期待,查询的输出也是关系型,并不保证顺序,
排序仅仅作为查询中计算描述的一部分,在结果中作为一特性产生。
1.1.3 窗口函数为什么优秀?orz
分组查询以聚合的形式,提供一些新的信息,代价是损失了详细信息。
当我们对数据进行分组时,不得不把计算应用到组中的所有内容上。但如果我们想在计算中涉及聚合信息和详细信息就GG
eg:
查询 Sales.OrderValues视图,计算每笔订单占当前客户订单总金额的百分比,以及与客户订单平均金额的差异。
当前订单金额是详细信息,客户订单总金额和平均金额是聚合值。如果按照客户来对数据进行分组,就获取不到每笔订单的金额
传统的分组查询对于这种需求的处理方式是:建立一个查询,按客户对数据进行分组,根据这个查询定义一个表表达式,把CTE表与基表联合
WITH a AS
(
SELECT custid, SUM(val) as sumval, AVG(val) as avgal
FROM Sales.OrderValues
GROUP BY custid
)
SELECT b.orderid, b.custid, b.val,
CAST((100. * b.val / a.sumval) as NUMERIC(5,2)) as pctcust,
b.val - a.avgal as diffcust
FROM Sales.OrderValues as b
JOIN a
ON a.custid = b.custid;
现在还想要计算占总合计的百分比以及与总平均金额的差异?再加一个表表达式。
注意这里再连接表的时候用CROSS JOIN因为总的
略
另一种就是对于每个计算使用独立的子查询
SELECT orderid, custid, val,
CAST(100. * val /
(SELECT SUM(o2.val)
FROM Sales.OrderValues as o2
WHERE o2.custid = o1.custid) as NUMERIC(5,2)) as pctcust,
val - (SELECT AVG(o2.val)
FROM Sales.OrderValues as o2
WHERE o2.custid = o1.custid) as diffcust
FROM Sales.OrderValues as o1;
......
这个代码太长性能也不好,需要两次扫库
SELECT *
FROM Sales.OrderValues as o1,Sales.OrderValues as o2
WHERE o2.custid = o1.custid
AND o2.custid = 85
自连接有5*5=25条记录
卧槽,这个思想很牛逼的一点就是通过SUM成倍扩大了一个用户的订单价格,然后val也出现了这个倍数次,相除倍数就没有了。
窗口函数背后的理念是要定义一个函数的操作窗口或行集。聚合函数也应该作用于行集。使用OVER子句来定义该函数的窗口
分区的含义是过滤而不是分组
1.返回当前订单占此客户订单总金额的百分比
2.val与该客户订单平均金额的差异
3.每笔订单占总金额百分比
4.每笔订单与总平均金额差异
SELECT orderid, custid, val,
CAST(100. * val / SUM(val) OVER (PARTITION BY custid) as NUMERIC(5,2)) as pctcust,
val - AVG(val) OVER (PARTITION by custid) as diffcust,
CAST(100. * val / SUM(val) OVER() as NUMERIC(5,2)) as pctall,
val - AVG(val) OVER () as diffall
FROM Sales.OrderValues
SQL Sever 如果发现不同的函数使用相同的窗口,只会对窗口内的数据访问一次。
还有一点事窗口函数的优点:添加限制前的初始窗口是查询的结果集。比如上面的查询想查看2007年,只须在查询中
增加一个筛选器。因为窗口函数的起始点是应用了过滤项之后的结果集,但是如果使用了子查询就需要在所有子查询中
重复使用筛选器。but,也可以使用一个CTE,该表应用了筛选器的查询,外部查询和子查询都指向这个CTE。
1.2 数据岛问题
SET nocount ON; --使返回的结果中不包含有关受 Transact-SQL 语句影响的行数的信息
USE TSQL2012;
IF OBJECT_ID('dbo.T1', 'U') IS NOT NULL DROP TABLE dbo.T1;
GO
CREATE TABLE dbo.T1
(
col1 INT NOT NULL
CONSTRAINT PK_T1 PRIMARY KEY
);
INSERT INTO dbo.T1(col1)
VALUES(2),(3),(11),(12),(13),(27),(33),(34),(35),(42);
GO
找到数据岛:两个方法,核心都是构建概念化的列,同一岛内grp值相同,以grp值分组。
法1:
对每个col,找到大于/等于当前值的最小col1值,并且要求这个值后面没有值。SO,一个数据岛
内的所有成员的组标识符就是这个数据岛最后一个成员的值。
SELECT col1,
(SELECT MIN(B.col1)
FROM dbo.T1 as B
WHERE B.col1 >= A.col1
AND NOT EXISTS (SELECT * FROM dbo.T1 as C
WHERE C.col1 = B.col1 + 1)) as grp
FROM dbo.T1 as A;
SELECT MIN(col1) as start_range, MAX(col1) as end_range
FROM (SELECT col1,
(SELECT MIN(B.col1)
FROM dbo.T1 as B
WHERE B.col1 >= A.col1
AND NOT EXISTS (SELECT *
FROM dbo.T1 as C
WHERE C.col1 = B.col1 + 1)) as grp
FROM dbo.T1 as A) as D
GROUP BY grp;
--发现可以在SELECT 里面不写分组的grp
这个查询问题在于逻辑负责,扫库两次,数据量大就歇菜
法2:窗口函数计算组标识符
SELECT col1, ROW_NUMBER() OVER(ORDER BY col1) as rownum
FROM dbo.T1;
这里发现col1不连贯, rownum是连贯的。在数据岛,两个序列都以固定的间隔在增长,因此二者的
差异是一个常数。
SELECT col1, col1 - ROW_NUMBER() OVER(ORDER BY col1) as diff
FROM dbo.T1;
这个差异可以当做组标识符用
SELECT MIN(col1) as start_range, MAX(col1) as end_range
FROM (SELECT col1, col1 - ROW_NUMBER() OVER(ORDER BY col1) as diff
FROM dbo.T1) as A
GROUP BY diff
--简单明了,仅包含一个在col1上的排序索引扫描和一个持续递增计数器的迭代器
1.3 窗口函数中的元素
分区 排序 框架
单独看下框架
计算每个员工每个订单月的销售数量累计总计:
SELECT empid, ordermonth, qty,
SUM(qty) OVER(PARTITION BY empid ORDER BY ordermonth
ROWS BETWEEN unbounded preceding AND CURRENT row) as runqty
FROM Sales.EmpOrders;
在分区内,按照给定的排序,设定框架为当前行之前的所有行(没有下边界点)。换句话说,结果合计的框架是当前行
(包含)之前的所有行
1.4 支持窗口函数的查询元素
一张图,二义性
不允许窗口函数判断遭遇SELECT
1.6 窗口定义的重复使用
SQL sever 不支持WINDOW
补充,找出一段时间内的连续日期区间,并且求持续天数:
/*
create table tmptable(id int identity(1,1),rq date)
insert tmptable values('2010.1.1')
insert tmptable values('2010.1.2')
insert tmptable values('2010.1.3')
insert tmptable values('2010.1.6')
insert tmptable values('2010.1.7')
insert tmptable values('2010.1.10')
insert tmptable values('2010.1.11')
insert tmptable values('2010.1.12')
insert tmptable values('2010.1.19')
insert tmptable values('2010.1.20')
insert tmptable values('2010.1.22')
insert tmptable values('2010.1.23')
insert tmptable values('2010.1.28')
--identity(1,1)自增字段
*/
--临时中间表
SELECT id, rq,
DATEDIFF(d,(SELECT MIN(rq) FROM tmptable), rq)+1 as idd
INTO #tp
FROM tmptable
rq idd
2010-01-01 1
2010-01-02 2
2010-01-03 3
2010-01-06 6
2010-01-07 7
2010-01-10 10
2010-01-11 11
2010-01-12 12
2010-01-19 19
2010-01-20 20
2010-01-22 22
2010-01-23 23
2010-01-28 28
通过日期差值,提取出日期,就可以应用数据岛问题的解决方法。
SELECT *--y.rq as startday, x.rq as endday, DATEDIFF(d, y.rq, x.rq)+1 dtdf
FROM (SELECT MIN(a.idd) as minid, MAX(a.idd) as maxid
FROM (SELECT idd, (idd - ROW_NUMBER() OVER (ORDER BY idd ASC)) id_diff
FROM #tp c
) a
GROUP BY a.id_diff
) z
LEFT JOIN #tp y
ON z.minid = y.idd
LEFT JOIN #tp x
ON z.maxid = x.idd
minid maxid id rq idd id1 rq1 idd1
1 3 1 2010-01-01 1 3 2010-01-03 3
6 7 4 2010-01-06 6 5 2010-01-07 7
10 12 6 2010-01-10 10 8 2010-01-12 12
19 20 9 2010-01-19 19 10 2010-01-20 20
22 23 11 2010-01-22 22 12 2010-01-23 23
28 28 13 2010-01-28 28 13 2010-01-28 28
startday endday dtdf
2010-01-01 2010-01-03 3
2010-01-06 2010-01-07 2
2010-01-10 2010-01-12 3
2010-01-19 2010-01-20 2
2010-01-22 2010-01-23 2
2010-01-28 2010-01-28 1