赞
踩
1)排名函数 rank(),dense_rank()、row_number() 静态窗口(不用frame)
2)聚合函数 avg()、count()、sum()、min()、max()、percent_rank()、cume_dist()
在我们日常数据分析中,常常遇到要在组内排名、组内取topn值等需求,例如下面的业务需求:
排名问题:每个品牌的商品按销售额来排名
topN问题:找出每个品牌排名前N的商品
面对这类需求,就需要使用sql的高级功能窗口函数了。窗口函数是个神奇的存在,可以把多行代码变一行处理,把复杂的子查询变简单。让我们来一起了解窗口函数吧!
窗口函数,也叫OLAP函数(Online Anallytical Processing,联机分析处理),作用于一个数据集合。窗口函数的一个概念就是当前行,当前行属于某个窗口就是从整个数据集选取一部分数据进行聚合/排名等操作。
- window_function_name(window_name/expression)
- OVER (
-
- [partition_defintion]
-
- [order_definition]
-
- [frame_definition])
窗口数据集由"[partition_defintion]","[order_definition]","[frame_definition]"确定。
静态窗口函数不能用frame子句;滑动窗口函数指加入order by或frame子句后,函数区域变为到当前行的数据集。
静态窗口函数:
排名函数 rank()、dense_rank()、row_number();
滑动窗口函数:
聚合函数 sum、 avg、count、max、min、percent_rank()、cum_dist();
取值函数 first_value()、last_value()、nth_value()、lag()、lead()、ntile()
函数名 | 参数 | 描述 |
---|---|---|
rank() | 无 | 这种排序允许并列,并且保留空缺,两个第1名,没有第2名,排名是1,1,3 |
dense_rank() | 无 | 这种排序允许并列,但不留空,两个第1名,接下来就是第2名,排名是1,1,2 |
row_number() | 无 | 这种排序不允许并列,且不留空,如果分数是100,100,99 ,这里的排名依旧是1,2,3 |
sum() | sum(expr) | 求和,expr指字段名称或表达式 |
avg() | avg(expr) | 取平均 |
count() | cout(expr) | 统计记录数 |
min() | min(expr) | 取最小值 |
max() | max(expr) | 取最大值 |
percent_rank() | 无 | 和之前的rank()函数相关,每条记录按照如下公式计算:(rank-1)/(rows-1)其中,rank为rank()函数产生的序号,rows为当前窗口的记录总行数 |
cum_dist() | 无 | 返回一组值中值的累积分布,表示值小于或等于行的值的行数除以总行数的行数 ,返回值大于零且小于或等于1 |
first_value() | first_value(expr) | 返回分组内的第一个值 |
last_value() | last_value(expr) | 返回分组内的最后一个值,与last_value()合称为头尾函数 |
nth_value() | nth_value(expr,n) | nth_value()功能与first_value()/last_value()类似,返回分组中第N个expr的值,n为正数,若n为空,函数将返回错误,若n大于窗口内的所有函数,函数返回NULL |
lag() | lag(expr, [n, [default]]) | 分区中位于当前行前n行的记录值,若n缺失默认值为1,如果没有返回default,没有default,返回NULL |
lead() | lead(expr, [n, [default]]) | 分区中位于当前行后n行的记录值,函数功能与lag()相反,其余与lag()相同 |
ntile() | ntile(n) | 再平均分配到n个集合中,返回每个记录的分桶号,范围从1到n |
窗口按照指定字段进行分区,分区语句为partition by <指定字段>,窗口函数功能在分区内执行,并在跨越分区边界时重新初始化。如果没有指定 partition by 语句,且没有后面的frame元素限制,就把所有数据当做一整个区。
按照指定字段进行排序,排序语句为order by <指定字段>[asc/desc]。和partition by 子句配合使用,就是对分区后的数据进行排序;如果单独使用且没有后面的frame元素的限制,就是对整个区的所有数据进行排序。
窗口框架的作用是对分区进一步细分,框架frame是当前分区的一个子集,在分区里面再进一步细分窗口,子句用来定义子集的规则,通常用来作为滑动窗口使用,某些窗口函数属于静态窗口,frame子句就没有作用。
frame_unit有两种,分别是ROWS和RANGE,ROWS是基于行号,RANGE是基于值的范围。
使用BETWEEN frame_start AND frame_end语法来表示行范围,frame_start和frame_end可以支持如下关键字:
接下来,结合实例,介绍以上几种窗口函数的用法
- -- 创建成绩表
- drop table if exists score;
- CREATE TABLE IF NOT EXISTS `score` (
- `学号` INT (5) zerofill NOT NULL,
- `姓名` VARCHAR (10) NOT NULL,
- `课程号` VARCHAR (15) NOT NULL,
- `成绩` INT (5) NOT NULL DEFAULT 0
- )ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='成绩表';
- -- 查看表结构
- DESC score;
接下来插入数据
- -- 插入数据
- INSERT INTO `score`(`学号`, `姓名`, `课程号`, `成绩`)
- VALUES
- ( '0005', '范若若', '00001', '99' ),( '0005', '范若若', '00002', '81' ),( '0005', '范若若', '00003', '81' ),
- ( '0001', '范闲', '00001', '95' ),( '0001', '范闲', '00002', '91' ),( '0001', '范闲', '00003', '90' ),
- ( '0004', '范思辙', '00001', '93' ),( '0004', '范思辙', '00002', '93' ),( '0004', '范思辙', '00003', '97' ),
- ( '0003', '王启年', '00001', '94' ),( '0003', '王启年', '00002', '84' ),( '0003', '王启年', '00003', '96' ),
- ( '0002', '林婉儿', '00001', '96' ),( '0002', '林婉儿', '00002', '86' ),( '0002', '林婉儿', '00003', '91' ),
- ( '0006', '叶轻眉', '00001', '100'),( '0006', '叶轻眉', '00002', '88' ),( '0006', '叶轻眉', '00003', '88' ),
- ( '0007', '庆帝', '00001', '98' ),( '0007', '庆帝', '00002', '88' ),( '0007', '庆帝', '00003', '93' );
- -- 查询成绩表
- select * from score order by 学号;
MySQL从8.0版本开始也和Sql Server、Oracle一样支持在查询中使用窗口函数,本文使用 MySQL 8.0.23版本通过实例来介绍窗口函数的应用。
- -- 按照姓名分区,成绩升序排名。 排名函数 rank(),dense_rank()、row_number()
- SELECT 姓名,课程号,成绩,
- rank() OVER (PARTITION BY 姓名 ORDER BY 成绩) AS r,
- dense_rank() OVER (PARTITION BY 姓名 ORDER BY 成绩) AS dr,
- row_number() OVER (PARTITION BY 姓名 ORDER BY 成绩) AS rn
- FROM score;
首先介绍常见聚合函数中的avg():求平均、count():统计个数、sum():求和、min():求最小值、max():求最大值,下面使用窗口函数和聚合函数求每个人成绩的均值、个数、总分、最小值和最大值。
- -- 求每个人成绩的均值、个数、总分、最小值和最大值
- SELECT *,
- avg(成绩) over w AS score_avg,
- count(成绩) over w AS score_count,
- sum(成绩) over w AS score_sum,
- min(成绩) over w AS score_min,
- max(成绩) over w AS score_max
- FROM score
- WINDOW w AS (partition by 姓名)
- order by 学号;
在上面这个例子中,通过对姓名分区后,聚合了各个课程的成绩,例如score_sum列 对每个人分区后,加总各个课程的分数得到总分,但结果中保留了每一行的信息,出现了重复的总分行。我们可以直接使用group by函数得到唯一的的信息,避免重复。
- -- 直接使用group by 函数
- SELECT 学号,姓名,
- round(avg(成绩),0) AS score_avg,
- count(成绩) AS score_count,
- sum(成绩) AS score_sum,
- min(成绩) AS score_min,
- max(成绩) AS score_max
- FROM score
- group by 姓名
- order by 学号;
下面介绍不太常用的两个聚合函数percent_rank():累计百分比、cume_dist():累计分布值函数
percent_rank(),和之前的rank()函数相关,每条记录按照如下公式计算:(rank-1)/(rows-1)其中,rank为RANK()函数产生的序号,rows为当前窗口的记录总行数。
cume_dist(),和之前的rank()函数也相关,每条记录按照如下公式计算:相对位置(行排名)/rows
例如:叶轻眉的课程号00002和00003的成绩一样都为88,属于同一个等级1。因此这些行都以第2行为基准:2/3=0.66666,如下图所示:
- -- percent_rank累计百分比、cume_dist累计分布值
- SELECT *,
- rank() over w r,
- percent_rank() over w AS pr,
- cume_dist() over w AS cd
- FROM score
- WINDOW w AS (partition by 姓名 order by 成绩);
注意:这两个函数不同的地方在于起始点,percent_rank 这个函数从0开始,而cume_list函数从1开始计算百分比。
用途:分区中位于当前行前n行/后n行的记录值。
使用场景:查询上一个订单距离当前订单的时间间隔等
- -- 按照课程号分区,成绩列每条记录的前一行lag()/后一行lead()
- SELECT *,
- lag(成绩, 1) over w as first_row,
- lead(成绩,1) over w as last_row
- FROM score
- WINDOW w AS (partition by 课程号);
注:lag(expr, [n, [default]])分区中位于当前行前n行的记录值,若n缺失默认值为1,如果没有返回default,没有default,返回NULL;lead()的语法和其相反,其余类似。
用途:返回分区中的第一个/最后一个记录值
- -- 按照课程号分区的第一个和最后一个成绩分数
- SELECT *,
- first_value(成绩) over w as first_values,
- last_value(成绩) over w as last_values
- FROM score
- WINDOW w AS (partition by 课程号);
(1)nth_value()
用途:返回窗口中第N个expr的值,expr可以是表达式,也可以是列名;
应用场景:每个用户订单中显示本用户金额排名第二和第三的订单金额,对头尾函数进行补充;
-
- -- 按照课程号分区取每个分区成绩的第2和第3个值
- SELECT *,
- nth_value(成绩, 2) over w as second_score,
- nth_value(成绩, 3) over w as third_score
- FROM score
- WINDOW w AS (partition by 课程号);
(2)ntile()
用途:将分区中的有序数据分为n个桶,记录桶号;
应用场景:将每个用户的订单按照订单金额分成3组。
此函数在数据分析中应用较多,比如由于数据量大,需要将数据平均分配到N个并行的进程分别计算,此时就可以用NTILE(N)对数据进行分组,由于记录数不一定被N整除,所以数据不一定完全平均。
- -- 按照课程号分区并按成绩升序排序,将各分区分为3桶
- SELECT *,
- ntile(3) over w as nt
- FROM score
- WINDOW w AS (partition by 课程号 order by 成绩 asc);
窗口函数非常有意思,对于一些使用常规思维无法实现的SQL需求,大家可以尝试一下窗口函数,相信会有意想不到的收获。而在MySQL8.0以前的版本上无法使用窗口函数,大家也可以试一试自己写窗口函数。
ROWS基于行号的应用
- -- 计算每分区内当前行的前1行(共两行)的平均值以及和,计算成绩的移动平均数与求和。
- SELECT 姓名,课程号,成绩,
- avg(成绩) OVER (PARTITION BY 姓名 ORDER BY 成绩 ASC ROWS 1 preceding) AS moving_avg,
- sum(成绩) OVER (PARTITION BY 姓名 ORDER BY 成绩 ASC ROWS 1 preceding) AS moving_sum
- FROM score;
RANGE是基于值范围的应用
- -- 计算每个分区内从第一行到当前行的平均值以及和
- SELECT 姓名,课程号,成绩,
- avg(成绩) OVER (PARTITION BY 姓名 ORDER BY 成绩 ASC RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS moving_avg,
- sum(成绩) OVER (PARTITION BY 姓名 ORDER BY 成绩 ASC RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS moving_sum
- FROM score;
- -- 计算每个分区内从当前行到最后一行的平均值以及和
- SELECT 姓名,课程号,成绩,
- avg(成绩) OVER (PARTITION BY 姓名 ORDER BY 成绩 ASC RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS moving_avg,
- sum(成绩) OVER (PARTITION BY 姓名 ORDER BY 成绩 ASC RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS moving_sum
- FROM score;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。