新特性解读 | 窗口函数的适用场景

时间:2022-07-28
本文章向大家介绍新特性解读 | 窗口函数的适用场景,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

作者:赵佳慧

爱可生售后团队成员,主要负责公司运维平台故障诊断。喜爱技术,努力在 IT 行业中磨练自己。

本文来源:原创投稿 *爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。


一、窗口函数

“窗口”可理解为记录集合。“窗口函数”可理解为在满足某种条件的记录集合上执行的特殊函数,对于每条记录都要在此窗口内执行。

二、快速理解窗口函数

通过举例,快速理解窗口函数。

2.1 举例

1)创建表 user,表 user 的数据如下:

mysql> select * from user;
+‐‐‐‐+‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
| id | name | address | createtime |
+‐‐‐‐+‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
| 1 | aa | cun | 2020‐06‐01 00:00:00 |
| 2 | bb | cun | 2020‐06‐01 00:00:00 |
| 3 | bb | shi | 2020‐06‐01 01:00:00 |
| 4 | bb | shi | 2020‐06‐01 01:00:00 |
| 5 | cc | cun | 2020‐06‐01 01:00:00 |
| 6 | tt | cun | 2020‐06‐03 01:00:00 |
| 7 | eee | cun | 2020‐06‐04 01:00:00 |
| 8 | eee | cun | 2020‐06‐04 01:00:00 |
| 9 | xx | shen | 2020‐06‐02 01:00:00 |
+‐‐‐‐+‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
9 rows in set (0.00 sec)

2)窗口函数的使用

SELECT createtime,
row_number() over(order by createtime) AS cr
FROM user ;
+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐+
| createtime | ll |
+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐+
| 2020‐06‐01 00:00:00 | 1 |
| 2020‐06‐01 00:00:00 | 2 |
| 2020‐06‐01 01:00:00 | 3 |
| 2020‐06‐01 01:00:00 | 4 |
| 2020‐06‐01 01:00:00 | 5 |
| 2020‐06‐02 01:00:00 | 6 |
| 2020‐06‐03 01:00:00 | 7 |
| 2020‐06‐04 01:00:00 | 8 |
| 2020‐06‐04 01:00:00 | 9 | 
+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐+
9 rows in set (0.00 sec)

row_number() over(order by createtime) as cr 这部分为窗口函数。over(order by createtime)为窗口规范,函数 row_number() 即对窗口的数据进行编号。所以上述 sql 的意思为:先对 createtime 进行排序,然后对每行数据进行编号。

三、窗口函数的适用场景

下面举例说明在哪些场景下适用窗口函数。

3.1 数据准备

1)创建用户表 user 并插入数据。

mysql> select * from user;
+‐‐‐‐+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
| id | name | address | createtime |
+‐‐‐‐+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
| 1 | 李四 | 广州 | 2020‐06‐01 00:00:00 |
| 2 | 老三 | 广西 | 2020‐06‐01 00:00:00 |
| 3 | 张三 | 广州 | 2020‐06‐01 01:00:00 |
| 4 | 王五 | 上海 | 2020‐06‐01 01:00:00 |
| 5 | 吴迪 | 深圳 | 2020‐06‐01 01:00:00 |
| 6 | 赵六 | 北京 | 2020‐06‐03 01:00:00 |
| 7 | 刘六 | 北京 | 2020‐06‐04 01:00:00 |
| 8 | 刘开 | 江西 | 2020‐06‐04 01:00:00 |
| 9 | 张劳 | 上海 | 2020‐06‐02 01:00:00 |
+‐‐‐‐+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
9 rows in set (0.00 sec)

2)创建交易表 transaction 并插入数据。

mysql> select * from transaction;
+‐‐‐‐+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+
| id | userid | amount | paydate |
+‐‐‐‐+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+
| 1 | 3 | 23 | 2020‐07‐01 |
| 2 | 1 | 34 | 2020‐07‐01 |
| 3 | 3 | 56 | 2020‐07‐01 |
| 4 | 3 | 32 | 2020‐07‐01 |
| 5 | 2 | 100 | 2020‐07‐03 |
| 6 | 3 | 100 | 2020‐07‐03 |
| 7 | 2 | 198 | 2020‐07‐01 |
| 8 | 4 | 209 | 2020‐07‐01 |
| 9 | 4 | 209 | 2020‐07‐03 |
| 10 | 4 | 209 | 2020‐07‐10 |
| 11 | 3 | 239 | 2020‐07‐14 |
| 12 | 6 | 100 | 2020‐07‐14 |
| 13 | 3 | 100 | 2020‐07‐30 |
| 14 | 3 | 140 | 2020‐07‐30 |
| 15 | 3 | 148 | 2020‐07‐30 |
| 16 | 3 | 89 | 2020‐07‐30 |
+‐‐‐‐+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+
16 rows in set (0.00 sec)

3.2 场景一

在拥有用户表和交易表的前提下,可以计算出每个用户的交易量占总交易量的百分比。由此我们可以快速得出每个用户的交易占比。

sql 如下:

SELECT a.name,
a.everymoney,
sum(a.everymoney) over() AS totalmoney,
a.everymoney/(sum(a.everymoney) over()) AS percent
FROM
(SELECT u.name,
sum(t.amount) everymoney
FROM user u
JOIN transaction t
ON u.id=t.userid
GROUP BY u.name) a;

+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+
| name | everymoney | totalmoney | percent |
+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+
| 张三 | 1694 | 6359 | 0.2664 |
| 李四 | 2014 | 6359 | 0.3167 |
| 老三 | 1491 | 6359 | 0.2345 |
| 王五 | 1060 | 6359 | 0.1667 |
| 赵六 | 100 | 6359 | 0.0157 |
+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+
5 rows in set (0.00 sec)

通过子查询 a 对用户进行分组计算出每个用户的总交易金额(everymoney),再通过子查询 aa 使用 sum()+over 子句计算出所有用户总的交易金额(totalmoney),最后将每个用户总交易金额比上所有用户总交易金额(a.everymoney/(sum(a.everymoney) over())。

3.3 场景二

在拥有用户表和交易表的前提下,可以计算出每天交易金额位于第一的用户。

sql 如下:

SELECT a.name,
a.paydate,
row_number()
OVER w AS num
FROM
(SELECT u.name,
paydate,
sum(amount) AS total
FROM user u
JOIN transaction t
ON u.id=t.userid
GROUP BY u.name,paydate) a window w AS (partition by paydate
ORDER BY a.total desc);

+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐+
| name | paydate | num |
+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐+
| 王五 | 2020‐07‐01 | 1 |
| 老三 | 2020‐07‐01 | 2 |
| 张三 | 2020‐07‐01 | 3 |
| 李四 | 2020‐07‐01 | 4 |
| 王五 | 2020‐07‐03 | 1 |
| 张三 | 2020‐07‐03 | 2 |
| 老三 | 2020‐07‐03 | 3 |
| 王五 | 2020‐07‐10 | 1 |
| 张三 | 2020‐07‐14 | 1 |
| 赵六 | 2020‐07‐14 | 2 |
| 张三 | 2020‐07‐30 | 1 |
+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐+
11 rows in set (0.00 sec)

上述 sql 通过子查询 a 对用户和 paydate 进行分组,计算出每个用户每天的交易金额

(total),再通过窗口函数 row_number() over w as num...window w as (partition by paydate order by a.total) 对数据按 paydate 分组,并按 total 降序排序进行编号。

由于按天分组,按交易金额降序进行编号,所以每个分组中的第一条就是交易额最高的,最终过滤 num=1 的数据即可得出每天交易金额最高的用户。

sql 如下:

SELECT *
FROM
(SELECT a.name,4
a.paydate,
row_number()
OVER w AS num
FROM
(SELECT u.name,
paydate,
sum(amount) AS total
FROM user u
JOIN transaction t
ON u.id=t.userid
GROUP BY u.name,paydate) a window w AS (partition by paydate
ORDER BY a.total)) aa
WHERE num=1;
+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐+
| name | paydate | num |
+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐+
| 王五 | 2020‐07‐01 | 1 |
| 王五 | 2020‐07‐03 | 1 |
| 王五 | 2020‐07‐10 | 1 |
| 张三 | 2020‐07‐14 | 1 |
| 张三 | 2020‐07‐30 | 1 |
+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐+
5 rows in set (0.00 sec)

通过如上的两个例子可以看出,窗口函数能够方便的实现一些排序及统计功能,当然也不局限于使用窗口函数,不过实现的 sql 语句可能会比较复杂。

四、窗口函数一览

MySQL 8.0 新增的窗口函数如下:

CUME_DIST()
DENSE_RANK()
FIRST_VALUE()
LAG()
LAST_VALUE()6
LEAD()
NTH_VALUE()
NTILE()
PERCENT_RANK()
RANK()
ROW_NUMBER()

4.1 CUME_DIST() 使用举例

cume_dist():表示当前行及小于当前行在窗口分区总行数中的占比。

举例:

mysql> select * from user1;
+‐‐‐‐+‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
| id | name | address | createtime |
+‐‐‐‐+‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
| 1 | aa | cun | 2020‐06‐01 00:00:00 |
| 2 | bb | cun | 2020‐06‐01 00:00:00 |
| 3 | bb | shi | 2020‐06‐01 01:00:00 |
| 4 | bb | shi | 2020‐06‐01 01:00:00 |
| 5 | cc | cun | 2020‐06‐01 01:00:00 |
| 6 | tt | cun | 2020‐06‐03 01:00:00 |
| 7 | eee | cun | 2020‐06‐04 01:00:00 |
| 8 | eee | cun | 2020‐06‐04 01:00:00 |
| 9 | xx | shen | 2020‐06‐02 01:00:00 |
+‐‐‐‐+‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
9 rows in set (0.00 sec)

SELECT id,
CUME_DIST()
OVER w AS ll,createtime
FROM user1 window w AS (order by createtime);
+‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
| id | ll | createtime |
+‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
| 1 | 0.2222222222222222 | 2020‐06‐01 00:00:00 |
| 2 | 0.2222222222222222 | 2020‐06‐01 00:00:00 |
| 3 | 0.5555555555555556 | 2020‐06‐01 01:00:00 |
| 4 | 0.5555555555555556 | 2020‐06‐01 01:00:00 |
| 5 | 0.5555555555555556 | 2020‐06‐01 01:00:00 |
| 9 | 0.6666666666666666 | 2020‐06‐02 01:00:00 |
| 6 | 0.7777777777777778 | 2020‐06‐03 01:00:00 |
| 7 | 1 | 2020‐06‐04 01:00:00 |
| 8 | 1 | 2020‐06‐04 01:00:00 |
+‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+
9 rows in set (0.00 sec)

如上 sql 所示,查看第一行数据值为 0.2222222222222222,因为小于等于 id=1 对应的 createtime 值有两行(id=1 和 id=2),所以经过 cume_dist() 函数计算后为 0.2222222222222222。

其他函数详细解析可前往官网查看:

https://dev.mysql.com/doc/refman/8.0/en/window-function-descriptions.html