MySQL--SQL高级处理

本文根据《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的结果中取出部分记录。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值