作者:赵佳慧

爱可生售后团队成员,主要负责公司运维平台故障诊断。喜爱技术,努力在 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 | 20200601 00:00:00 || 3 | bb | shi | 2020‐06‐01 01:00:00 || 4 | bb | shi | 20200601 01:00:00 || 5 | cc | cun | 2020‐06‐01 01:00:00 || 6 | tt | cun | 20200603 01:00:00 || 7 | eee | cun | 2020‐06‐04 01:00:00 || 8 | eee | cun | 20200604 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 crFROM user ;+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐+| createtime | ll |+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐+| 20200601 00:00:00 | 1 || 2020‐06‐01 00:00:00 | 2 || 20200601 01:00:00 | 3 || 2020‐06‐01 01:00:00 | 4 || 20200601 01:00:00 | 5 || 2020‐06‐02 01:00:00 | 6 || 20200603 01:00:00 | 7 || 2020‐06‐04 01:00:00 | 8 || 20200604 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 | 老三 | 广西 | 20200601 00:00:00 || 3 | 张三 | 广州 | 2020‐06‐01 01:00:00 || 4 | 王五 | 上海 | 20200601 01:00:00 || 5 | 吴迪 | 深圳 | 2020‐06‐01 01:00:00 || 6 | 赵六 | 北京 | 20200603 01:00:00 || 7 | 刘六 | 北京 | 2020‐06‐04 01:00:00 || 8 | 刘开 | 江西 | 20200604 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 | 20200701 || 3 | 3 | 56 | 2020‐07‐01 || 4 | 3 | 32 | 20200701 || 5 | 2 | 100 | 2020‐07‐03 || 6 | 3 | 100 | 20200703 || 7 | 2 | 198 | 2020‐07‐01 || 8 | 4 | 209 | 20200701 || 9 | 4 | 209 | 2020‐07‐03 || 10 | 4 | 209 | 20200710 || 11 | 3 | 239 | 2020‐07‐14 || 12 | 6 | 100 | 20200714 || 13 | 3 | 100 | 2020‐07‐30 || 14 | 3 | 140 | 20200730 || 15 | 3 | 148 | 2020‐07‐30 || 16 | 3 | 89 | 20200730 |+‐‐‐‐+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+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 percentFROM(SELECT u.name,sum(t.amount) everymoneyFROM user uJOIN transaction tON u.id=t.useridGROUP 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 numFROM(SELECT u.name,paydate,sum(amount) AS totalFROM user uJOIN transaction tON u.id=t.useridGROUP BY u.name,paydate) a window w AS (partition by paydateORDER BY a.total desc);
+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐+| name | paydate | num |+‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐+| 王五 | 20200701 | 1 || 老三 | 20200701 | 2 || 张三 | 20200701 | 3 || 李四 | 20200701 | 4 || 王五 | 20200703 | 1 || 张三 | 20200703 | 2 || 老三 | 20200703 | 3 || 王五 | 20200710 | 1 || 张三 | 20200714 | 1 || 赵六 | 20200714 | 2 || 张三 | 20200730 | 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,4a.paydate,row_number()OVER w AS numFROM(SELECT u.name,paydate,sum(amount) AS totalFROM user uJOIN transaction tON u.id=t.useridGROUP BY u.name,paydate) a window w AS (partition by paydateORDER BY a.total)) aaWHERE 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()6LEAD()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 | 20200601 00:00:00 || 3 | bb | shi | 2020‐06‐01 01:00:00 || 4 | bb | shi | 20200601 01:00:00 || 5 | cc | cun | 2020‐06‐01 01:00:00 || 6 | tt | cun | 20200603 01:00:00 || 7 | eee | cun | 2020‐06‐04 01:00:00 || 8 | eee | cun | 20200604 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,createtimeFROM 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