本文根据《SQL基础教程》第二版写,里面的概念、代码、例子均来自此书。
1 窗口函数
- 窗口函数可以进行排序、生成序列号等一般的聚合函数无法实现的高级操作
- 理解partition by和order by 这两个关键字
什么是窗口函数
窗口函数也称OLAP(online analytical procesing)函数,是对数据库数据进行试试分析处理的函数。
很可惜,MySQL5.7不支持窗口函数,故下载了8.0版本
窗口函数的语法
<窗口函数> over ([partition by <列清单>] order by <排序用列清单>)
窗口函数大致分为两种:
- 能够作为窗口函数的聚合函数(sum,avg,count,max,min)
- rank,dense_rank,row_number等专用窗口函数
语法的基本使用方法–使用rank函数
rank是用来计算记录排序的函数
例如,对之前使用过的product表中的8件商品,我们根据不同的商品种类,按照销售单价从低到高的顺序排序:
mysql> select product_name,product_type,sale_price,rank()
-> over (partition by product_type order by sale_price) as ranking
-> from product;
+--------------+--------------+------------+---------+
| product_name | product_type | sale_price | ranking |
+--------------+--------------+------------+---------+
| 圆珠笔 | 办公用品 | 100 | 1 |
| 打孔器 | 办公用品 | 500 | 2 |
| 叉子 | 厨房用具 | 500 | 1 |
| 擦菜板 | 厨房用具 | 880 | 2 |
| 菜刀 | 厨房用具 | 3000 | 3 |
| 高压锅 | 厨房用具 | 6800 | 4 |
| T恤 | 衣服 | 1000 | 1 |
| 运动T恤 | 衣服 | 4000 | 2 |
+--------------+--------------+------------+---------+
在这里,partition by能够设定排序的对象范围,order by能够指定哪一列、何种顺序进行排序
窗口函数中的order by与select语句末尾中的一样,可以通过关键字asc/desc来指定升序和降序。
==partition by在横向上对表进行分组,而order by决定了纵向排序的规则。==窗口函数兼具分组和排序两个功能。但是partition by并不具备group by子句的汇总功能,因此使用rank函数并不会减少原表中记录的行数。通过partition by分组后的记录集合称为窗口,此窗口并不是窗户的意思,而是代表范围的意思。此外,各个窗口在定义上是绝对不会包含共通的部分,这与通过group by子句分割后的集合具有相同的特征。
无需指定partition by
窗口函数起到关键作用的是partition by和group by。其中,partition by并不是必须的,即使不指定也可以正常使用窗口函数。
mysql> select product_name,product_type,sale_price,rank()
-> over (order by sale_price) as ranking
-> from product;
+--------------+--------------+------------+---------+
| product_name | product_type | sale_price | ranking |
+--------------+--------------+------------+---------+
| 圆珠笔 | 办公用品 | 100 | 1 |
| 打孔器 | 办公用品 | 500 | 2 |
| 叉子 | 厨房用具 | 500 | 2 |
| 擦菜板 | 厨房用具 | 880 | 4 |
| T恤 | 衣服 | 1000 | 5 |
| 菜刀 | 厨房用具 | 3000 | 6 |
| 运动T恤 | 衣服 | 4000 | 7 |
| 高压锅 | 厨房用具 | 6800 | 8 |
+--------------+--------------+------------+---------+
从结果可以看出,之前我们得到的是按照商品种类分组后的排序,而这次变成了全部商品的排序。像这样,当希望先将表中的数据分为多个部分(窗口),再使用窗口函数时,可以使用partition by选项。
专用窗口函数的种类
上述例子中,观察结果,可以看出打孔器和叉子都排在第2位,擦菜板排在第4位。
rank函数
计算排序时,如果存在相同位次的记录,则会跳过之后的位次。例如,有3条记录排在第一位时:1位,1位,1位,4位。
dense_rank函数
计算排序,如果存在相同位次的记录,也不会跳过之后的位次。例如:1位,1位,1位,2位。
row_number函数
赋予唯一的连续位次。例如有三条记录排在第一位时,1位,2位,3位,4位。
上述三个函数在所有的DBMS中都能使用。下面我们使用上述三个函数来看一下效果:
mysql> select product_name,product_type,sale_price,
-> rank() over (order by sale_price) as ranking,
-> dense_rank() over (order by sale_price) as dense_ranking,
-> row_number() over (order by sale_price) as row_num
-> from product;
+--------------+--------------+------------+---------+---------------+---------+
| product_name | product_type | sale_price | ranking | dense_ranking | row_num |
+--------------+--------------+------------+---------+---------------+---------+
| 圆珠笔 | 办公用品 | 100 | 1 | 1 | 1 |
| 打孔器 | 办公用品 | 500 | 2 | 2 | 2 |
| 叉子 | 厨房用具 | 500 | 2 | 2 | 3 |
| 擦菜板 | 厨房用具 | 880 | 4 | 3 | 4 |
| T恤 | 衣服 | 1000 | 5 | 4 | 5 |
| 菜刀 | 厨房用具 | 3000 | 6 | 5 | 6 |
| 运动T恤 | 衣服 | 4000 | 7 | 6 | 7 |
| 高压锅 | 厨房用具 | 6800 | 8 | 7 | 8 |
+--------------+--------------+------------+---------+---------------+---------+
专用窗口函数无需参数,因此括号中都是空的。
窗口函数的适用范围
原则上窗口函数只能在select子句中使用(也就是说不能在where子句或者group by子句中使用)
作为窗口函数使用的聚合函数
所有的聚合函数都能用作窗口函数,其语法和专用窗口函数完全相同。例如,我们将sum函数作为窗口函数的使用:
mysql> select product_id,product_name,sale_price,
-> sum(sale_price) over (order by product_id) as current_sum
-> from product;
+------------+--------------+------------+-------------+
| product_id | product_name | sale_price | current_sum |
+------------+--------------+------------+-------------+
| 0001 | T恤 | 1000 | 1000 |
| 0002 | 打孔器 | 500 | 1500 |
| 0003 | 运动T恤 | 4000 | 5500 |
| 0004 | 菜刀 | 3000 | 8500 |
| 0005 | 高压锅 | 6800 | 15300 |
| 0006 | 叉子 | 500 | 15800 |
| 0007 | 擦菜板 | 880 | 16680 |
| 0008 | 圆珠笔 | 100 | 16780 |
+------------+--------------+------------+-------------+
在按照时间序列的顺序,计算各个时间的销售额总额的时候,通常都会使用这种称为累计的统计方法。
将avg函数作为窗口函数使用:
mysql> select product_id,product_name,sale_price,
-> avg(sale_price) over (order by product_id) as current_avg
-> from product;
+------------+--------------+------------+-------------+
| product_id | product_name | sale_price | current_avg |
+------------+--------------+------------+-------------+
| 0001 | T恤 | 1000 | 1000.0000 |
| 0002 | 打孔器 | 500 | 750.0000 |
| 0003 | 运动T恤 | 4000 | 1833.3333 |
| 0004 | 菜刀 | 3000 | 2125.0000 |
| 0005 | 高压锅 | 6800 | 3060.0000 |
| 0006 | 叉子 | 500 | 2633.3333 |
| 0007 | 擦菜板 | 880 | 2382.8571 |
| 0008 | 圆珠笔 | 100 | 2097.5000 |
+------------+--------------+------------+-------------+
后面的平均分别是前几个加起来的平均。
像这样以==“自身记录(当前记录)”作为基准==进行统计,就是将聚合函数当做窗口函数使用时的最大特征。
计算移动平均
窗口函数就是将表以窗口为单位进行分割,并在其中进行排序的函数。其实其中还包含在窗口中指定更加详细的汇总范围的备选功能,该备选功能中的汇总范围称为框架。==需要在order by子句之后使用指定范围的关键字。
例如,指定最靠近的三行作为汇总对象:
mysql> select product_id,product_name,sale_price,
-> avg(sale_price) over (order by product_id rows 2 preceding) as moving_avg
-> from product;
+------------+--------------+------------+------------+
| product_id | product_name | sale_price | moving_avg |
+------------+--------------+------------+------------+
| 0001 | T恤 | 1000 | 1000.0000 |
| 0002 | 打孔器 | 500 | 750.0000 |
| 0003 | 运动T恤 | 4000 | 1833.3333 |
| 0004 | 菜刀 | 3000 | 2500.0000 |
| 0005 | 高压锅 | 6800 | 4600.0000 |
| 0006 | 叉子 | 500 | 3433.3333 |
| 0007 | 擦菜板 | 880 | 2726.6667 |
| 0008 | 圆珠笔 | 100 | 493.3333 |
+------------+--------------+------------+------------+
其中,moving_avg 中1833.3333=(1000+500+400)/3
2500=(500+4000+3000)/3
等到,以此类推
这里我们使用了row(行)和preceding(之前)两个关键字,将框架指定为“截止到之前……行”,因此“row 2 preceding”就是将框架指定为“截止到之前两行”,也就是将作为汇总对象的记录限定为如下的“最靠近的3行”:自身(当前记录)、之前1行的记录、之前2行的记录。
使用关键字follwling(之后)替换preceding,就可以指定“截止到之后……行”作为框架了。
如果希望将当前记录的前后行作为汇总对象时,就可以同时使用preceding和folowing:
mysql> select product_id,product_name,sale_price,
-> avg(sale_price) over (order by product_id rows between 1 preceding and 1 following) as moving_avg
-> from product;
+------------+--------------+------------+------------+
| product_id | product_name | sale_price | moving_avg |
+------------+--------------+------------+------------+
| 0001 | T恤 | 1000 | 750.0000 |
| 0002 | 打孔器 | 500 | 1833.3333 |
| 0003 | 运动T恤 | 4000 | 2500.0000 |
| 0004 | 菜刀 | 3000 | 4600.0000 |
| 0005 | 高压锅 | 6800 | 3433.3333 |
| 0006 | 叉子 | 500 | 2726.6667 |
| 0007 | 擦菜板 | 880 | 493.3333 |
| 0008 | 圆珠笔 | 100 | 490.0000 |
+------------+--------------+------------+------------+
其中,2500=(500+4000+3000)/3,以此类推。
上面的代码,我们指定将“1 preceding(之前1行)”和“1 following(之后1行)”的区间作为汇总对象。具体来说三行为:之前1行的记录、自身、之后1行的记录
两个order by
使用窗口函数时必须要在over子句中使用order by,但是结果中的记录有时候不会按照order by指定的顺序进行排序。over子句中的order by只是用来决定窗口函数按照什么样的顺序进行计算的,对结果排序顺序并没有影响。
mysql> select product_name,product_type,sale_price,
-> rank() over (order by sale_price) as ranking
-> from product
-> order by ranking desc;
+--------------+--------------+------------+---------+
| product_name | product_type | sale_price | ranking |
+--------------+--------------+------------+---------+
| 高压锅 | 厨房用具 | 6800 | 8 |
| 运动T恤 | 衣服 | 4000 | 7 |
| 菜刀 | 厨房用具 | 3000 | 6 |
| T恤 | 衣服 | 1000 | 5 |
| 擦菜板 | 厨房用具 | 880 | 4 |
| 打孔器 | 办公用品 | 500 | 2 |
| 叉子 | 厨房用具 | 500 | 2 |
| 圆珠笔 | 办公用品 | 100 | 1 |
+--------------+--------------+------------+---------+
mysql> select product_name,product_type,sale_price,
-> rank() over (order by sale_price desc) as ranking
-> from product;
+--------------+--------------+------------+---------+
| product_name | product_type | sale_price | ranking |
+--------------+--------------+------------+---------+
| 高压锅 | 厨房用具 | 6800 | 1 |
| 运动T恤 | 衣服 | 4000 | 2 |
| 菜刀 | 厨房用具 | 3000 | 3 |
| T恤 | 衣服 | 1000 | 4 |
| 擦菜板 | 厨房用具 | 880 | 5 |
| 打孔器 | 办公用品 | 500 | 6 |
| 叉子 | 厨房用具 | 500 | 6 |
| 圆珠笔 | 办公用品 | 100 | 8 |
+--------------+--------------+------------+---------+
2 grouping运算符
- 只使用group by 子句饿聚合函数是无法同时得出小计和合计的,如果想要同时得到,可以使用grouping运算符。
- grouping运算符中cube的关键在于形成“积木搭建出的立方体” 的印象。
- 有些DBMS不支持grouping这一功能。
同时得到合计行
mysql> select product_type,sum(sale_price) from product
-> group by product_type;
+--------------+-----------------+
| product_type | sum(sale_price) |
+--------------+-----------------+
| 衣服 | 5000 |
| 办公用品 | 600 |
| 厨房用具 | 11180 |
+--------------+-----------------+
像这样,虽然有每个种类的和,但是没有总的合计。按照以前的做法,我们可以使用union。将两个结果连接在一起。
mysql> select '合计' as product_type,sum(sale_price) from product
-> union all
-> select product_type,sum(sale_price) from product group by product_type;
+--------------+-----------------+
| product_type | sum(sale_price) |
+--------------+-----------------+
| 合计 | 16780 |
| 衣服 | 5000 |
| 办公用品 | 600 |
| 厨房用具 | 11180 |
+--------------+-----------------+
rollup–同时得出合计和小计
grouping运算符包含了一下3种:
- rollup
- cube
- grouping sets
MySQL仅支持rollup
rollup的使用方法
例如使用rollup同时得出合计和小计
mysql> select product_type,sum(sale_price) as sum_price
-> from product
-> group by product_type with rollup;
+--------------+-----------+
| product_type | sum_price |
+--------------+-----------+
| 办公用品 | 600 |
| 厨房用具 | 11180 |
| 衣服 | 5000 |
| NULL | 16780 |
+--------------+-----------+
-- 其他数据库中
select product_type,sum(sale_price) as sum_price
from product
group by rollup(product_type);
从语法上讲,就是将group by子句中的聚合键清单像rollup(<列1>,<列2>,……)这样使用。该运算符的作用就是“一次计算出不同聚合键组合的结果”。例如本例就是一次计算出了如下两种组合的汇总结果。
1、group by()
2、group by(product_type)
1中的group by()表示没有聚合键,也就相当于没有group by子句(这时会得到全部数据的合计行的记录),该合计行记录称为超级分组记录。此键值会默认使用null。
超级分组记录默认使用null作为聚合键
将登记日期添加到聚合键当中
在group by中添加登记日期(不适用rollup):
mysql> select product_type,regist_date,sum(sale_price) as sum_price
-> from product
-> group by product_type,regist_date;
+--------------+-------------+-----------+
| product_type | regist_date | sum_price |
+--------------+-------------+-----------+
| 衣服 | 2009-09-20 | 1000 |
| 办公用品 | 2009-09-11 | 500 |
| 衣服 | NULL | 4000 |
| 厨房用具 | 2009-09-20 | 3500 |
| 厨房用具 | 2009-01-15 | 6800 |
| 厨房用具 | 2008-04-28 | 880 |
| 办公用品 | 2009-11-11 | 100 |
+--------------+-------------+-----------+
使用rollup:
mysql> select product_type,regist_date,sum(sale_price) as sum_price
-> from product
-> group by product_type,regist_date with rollup;
+--------------+-------------+-----------+
| product_type | regist_date | sum_price |
+--------------+-------------+-----------+
| 办公用品 | 2009-09-11 | 500 |
| 办公用品 | 2009-11-11 | 100 |
| 办公用品 | NULL | 600 |
| 厨房用具 | 2008-04-28 | 880 |
| 厨房用具 | 2009-01-15 | 6800 |
| 厨房用具 | 2009-09-20 | 3500 |
| 厨房用具 | NULL | 11180 |
| 衣服 | NULL | 4000 |
| 衣服 | 2009-09-20 | 1000 |
| 衣服 | NULL | 5000 |
| NULL | NULL | 16780 |
+--------------+-------------+-----------+
将结果进行比较,我们发现使用rollup时多出了总的合计以及3条不同商品种类的小计行(也就是未使用登记日期作为聚合键的记录),这4行就是超级分组记录。相当于;
1、group by()
2、group by(product_type)
3、group by(product_type,regist_date)
rollup从最小的聚合级开始,聚合单位逐渐扩大。rollup可以同时得出合计和小计,很方便。
grouping函数–让null更加容易分辨
在上面的例子中,衣服的regist_date列中,有两个null,一个本身是null,一个是因为超级分组记录。为了避免混淆,grouping可用来判断超级分组的null,该函数在其参数列的值为超级分组记录所产生的null时返回1,其他情况返回0
mysql> select grouping(product_type) as product_type,grouping(regist_date) as regist_date,sum(sale_price) as sum_price
-> from product
-> group by product_type,regist_date with rollup;
+--------------+-------------+-----------+
| product_type | regist_date | sum_price |
+--------------+-------------+-----------+
| 0 | 0 | 500 |
| 0 | 0 | 100 |
| 0 | 1 | 600 |
| 0 | 0 | 880 |
| 0 | 0 | 6800 |
| 0 | 0 | 3500 |
| 0 | 1 | 11180 |
| 0 | 0 | 4000 |
| 0 | 0 | 1000 |
| 0 | 1 | 5000 |
| 1 | 1 | 16780 |
+--------------+-------------+-----------+
这样就可以分辨超级分组记录中的null和原始数据本身的null了。使用grouping函数还能在超级分组的键值中插入字符串,也就是说,当grouping函数的返回值为1时,指定“合计”或者“小计”等字符串,其他情况返回通常的列的值。
例如,在超级分组记录的键值汇总插入恰当的字符串:
mysql> select case when grouping(product_type)=1
-> then '商品种类 合计'
-> else product_type end as product_type,
-> case when grouping(regist_date)=1
-> then '登记日期 合计'
-> else cast(regist_date as varchar(16)) end as regist_date,
-> sum(sale_price) as sum_price
-> from product
-> group by product_type,regist_date with rollup;
(执行出错,不知道哪错了)
cast(regist_date as varchar(16))
为什么要用cast?这是为了满足case表达式所有分支的返回值必须一致的条件。
cube–用数据来搭积木
cube语法和rollup相同,只需要将rollup替换成cube就可以了。
mysql> select case when grouping(product_type)=1
-> then '商品种类 合计'
-> else product_type end as product_type,
-> case when grouping(regist_date)=1
-> then '登记日期 合计'
-> else cast(regist_date as varchar(16)) end as regist_date,
-> sum(sale_price) as sum_price
-> from product
-> group by product_type,regist_date with cube;
(报错,原因不详)
结果中,与rollup结果相比,cube中多出了几行记录,多出的几行记录就是只把regist_date作为聚合键所得到的汇总结果。
所谓cube,就是将group by子句中聚合键的“所有可能的组合”的汇总结果集中到一个结果中。在此例中,组合的个数就是2的n次方为2 的2次方,结果为4,。使用rollup时组合的个数为n+1(n是聚合键的个数)
grouping sets–取得期望的积木
该函数可以从rollup或cube的结果中取出部分记录。