怎么样得到各部门工资排名前N名员工列表?
查找各部门每人工资占部门总工资的百分比?
累计求和如何计算?
连续统计N天登陆的用户?
以上类型问题均使用窗口函数可以解决,这类需求都有一个共同的特点,需要在单表中满足某些条件的结果集内部做一些函数操作,
对分组统计结果中的每一条记录进行计算的场景下, 使用窗口函数更好, 注意, 是每一条!! 因为MySQL的普通聚合函数的结果(如 group> by)是每一组只有一条记录!!!
窗口函数的作用类似于在查询中对数据进行分组,不同的是,分组操作会把分组的结果聚合成一条记录,而窗口函数是将分组的结果置于每一条数据记录中。
静态窗口函数的窗口大小是固定的, 不会因为记录的不同而不同;
动态窗口函数的窗口大小会随着记录的不同而变化;
窗口函数总体上可以分为序号函数, 分布函数, 前后函数, 首尾函数和其他函数;
function over (partition by 字段a order by 字段b RANGE|ROWS BETWEEN start AND end)
OVER 关键字指定窗口的范围;
如果省略后面括号中的内容,则窗口会包含满足WHERE条件的所有记录,窗口函数会基于所有满足WHERE条件的记录进行计算。
如果OVER关键字后面的括号不为空,则可以使用如下语法设置窗口:
PARTITION BY 子句: 指定窗口函数按照哪些字段进行分组,分组后, 窗口函数可以在每个分组中分别执行;
ORDER BY 子句: 指定窗口函数按照哪些字段进行排序,
执行排序操作使窗口函数按照排序后的数据记录的顺序进行编号;
FRAME 子句: 为分区中的某个子集定义规则, 可以用来作为滑动窗口使用;
窗口函数基于查询结果的行数据进行计算,窗口函数运行在HAVING子句之后、 ORDER BY子句之前。窗口函数需要特殊的关键字OVER子句来指定窗口即触发一个窗口函数。
序号函数 | 功能 |
---|---|
row_number() | 顺序排序:对数据中的序号进行顺序显示,不管其排序结果是否出现重复值,排序结果为1,2,3,4,5… |
rank() | 并列排序:相同字段数值并列排序,且跳过重复序号,如, 1,1,3,4,5 。rank函数没有参数,但需要指定按照那个字段进行排名,所以使用rank函数必须用order by参数,order by的排序字段就是排名字段 |
dense_rank() | 并列排序:相同字段数值并列排序,且不跳过重复序号,如:1,1,2,3,4 |
代码如下:
CREATE TABLE examination_info (
id int PRIMARY KEY AUTO_INCREMENT COMMENT '自增ID',
exam_id int UNIQUE NOT NULL COMMENT '试卷ID',
tag varchar(32) COMMENT '类别标签',
difficulty varchar(8) COMMENT '难度',
duration int NOT NULL COMMENT '时长',
release_time datetime COMMENT '发布时间'
)CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE user_info (
id int PRIMARY KEY AUTO_INCREMENT COMMENT '自增ID',
uid int UNIQUE NOT NULL COMMENT '用户ID',
`nick_name` varchar(64) COMMENT '昵称',
achievement int COMMENT '成就值',
level int COMMENT '用户等级',
job varchar(32) COMMENT '职业方向',
register_time datetime COMMENT '注册时间'
)CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE exam_record (
id int PRIMARY KEY AUTO_INCREMENT COMMENT '自增ID',
uid int NOT NULL COMMENT '用户ID',
exam_id int NOT NULL COMMENT '试卷ID',
start_time datetime NOT NULL COMMENT '开始时间',
submit_time datetime COMMENT '提交时间',
score tinyint COMMENT '得分'
)CHARACTER SET utf8 COLLATE utf8_general_ci;
INSERT INTO user_info(uid,`nick_name`,achievement,`level`,job,register_time) VALUES
(1001, '牛客1', 3200, 7, '算法', '2020-01-01 10:00:00'),
(1002, '牛客2号', 2500, 6, '算法', '2020-01-01 10:00:00'),
(1003, '牛客3号♂', 2200, 5, '算法', '2020-01-01 10:00:00');
INSERT INTO examination_info(exam_id,tag,difficulty,duration,release_time) VALUES
(9001, 'SQL', 'hard', 60, '2020-01-01 10:00:00'),
(9002, 'SQL', 'hard', 80, '2020-01-01 10:00:00'),
(9003, '算法', 'hard', 80, '2020-01-01 10:00:00'),
(9004, 'PYTHON', 'medium', 70, '2020-01-01 10:00:00');
INSERT INTO exam_record(uid,exam_id,start_time,submit_time,score) VALUES
(1001, 9001, '2020-01-01 09:01:01', '2020-01-01 09:21:59', 90),
(1002, 9001, '2020-01-20 10:01:01', null, null),
(1002, 9001, '2020-02-01 12:11:01', null, null),
(1003, 9001, '2020-03-01 19:01:01', null, null),
(1001, 9001, '2020-03-01 12:01:01', null, null),
(1002, 9001, '2020-03-01 12:01:01', '2020-03-01 12:41:01', 90),
(1002, 9001, '2020-05-02 19:01:01', '2020-05-02 19:32:00', 90),
(1001, 9002, '2020-01-02 19:01:01', '2020-01-02 19:59:01', 69),
(1001, 9002, '2020-02-02 12:01:01', '2020-02-02 12:20:01', 99),
(1002, 9002, '2020-02-02 12:01:01', null, null),
(1002, 9002, '2020-02-02 12:01:01', '2020-02-02 12:43:01', 81),
(1002, 9002, '2020-03-02 12:11:01', null, null),
(1001, 9001, '2020-01-02 10:01:01', '2020-01-02 10:31:01', 89),
(1001, 9002, '2020-01-01 12:11:01', null, null),
(1002, 9001, '2020-01-01 18:01:01', '2020-01-01 18:59:02', 90),
(1002, 9003, '2020-05-06 12:01:01', null, null),
(1001, 9002, '2020-05-05 18:01:01', null, null);
select nick_name,ei.exam_id,score,
row_number() over(partition by nick_name order by score desc) row_ranking,
rank() over(partition by nick_name order by score desc) ranking,
dense_rank() over(partition by nick_name order by score desc) dense_ranking
from user_info ui
join exam_record er on ui.uid = er.uid
join examination_info ei on er.exam_id = ei.exam_id
where score is not null
percent_rank() | **累计百分比。**函数计算结果为:小于该条记录值的所有记录的行数/该分组的总行数-1,所以该记录的返回值为[0,1]。和之前的RANK()函数相关,每行按照如下公式进行计算: (rank - 1) / (rows - 1) 其中,rank为RANK()函数产生的序号,rows为当前窗口的记录总行数。 |
cume_dist() | **累计分布值。**分组值小于等于当前值的行数与分组总行数的比值 ,(0,1]。 分组内大于等于当前rank值的行数/分组内总行数。(常用) |
班级中比当前同学成绩高的学生比例是多少?
select nick_name,ei.exam_id,score,
PERCENT_RANK() OVER (PARTITION BY nick_name
ORDER BY score DESC) as percent,
CUME_DIST() OVER (PARTITION BY nick_name
ORDER BY score DESC) as cumdist
from user_info ui
join exam_record er on ui.uid = er.uid
join examination_info ei on er.exam_id = ei.exam_id
where score is not null
lead(字段,n) / lag(字段,n):分组中位于当前行后n行(lead)/ 前n行(lag)的记录值。
字段可以不填写,n(也叫offset)是从当前行偏移的行数,以获取值。offset必须是一个非负整数。如果offset为零,则LAG()函数计算当前行的值。如果省略 offset,则LAG()函数默认使用n=1, 向前看一个数据。
场景:求每个用户相邻两次浏览的时间差
求用户当前测试成绩的前一次测试成绩和后一次成绩
select nick_name,ei.exam_id,score,
lead(score,1) OVER (PARTITION BY nick_name
ORDER BY score DESC) as leadVal,
lag(score,1) OVER (PARTITION BY nick_name
ORDER BY score DESC) as lagVal
from user_info ui
join exam_record er on ui.uid = er.uid
join examination_info ei on er.exam_id = ei.exam_id
where score is not null
first_val(expr) / last_val(expr):得到分区中的第一个/最后一个指定参数的值
select nick_name,ei.exam_id,score,
FIRST_VALUE(score) OVER (PARTITION BY nick_name
ORDER BY score DESC) as firstVal,
LAST_VALUE(score) OVER (PARTITION BY nick_name
ORDER BY score DESC) as lastVal
from user_info ui
join exam_record er on ui.uid = er.uid
join examination_info ei on er.exam_id = ei.exam_id
where score is not null
聚和窗口函数和上面提到的专用窗口函数用法完全相同,只需要把聚合函数写在窗口函数的位置即可,但是函数后面括号里面不能为空,需要指定聚合的列名。
不改变表结构
函数 | 功能 |
---|---|
min() | 计算最小值 |
max() | 计算最大值 |
count() | 计数 |
sum() | 求和 |
avg | 求平均值 |
select *,
sum(成绩) over (order by 学号) as current_sum,
avg(成绩) over (order by 学号) as current_avg,
count(成绩) over (order by 学号) as current_count,
max(成绩) over (order by 学号) as current_max,
min(成绩) over (order by 学号) as current_min
from 班级表
nth_value(expr, n):返回窗口中第N个expr的值,expr可以是表达式,也可以是列名
排名第一和第二的分数
select nick_name,ei.exam_id,score,
nth_value(score,1) OVER (PARTITION BY nick_name
ORDER BY score DESC) as 1th,
nth_value(score,2) OVER (PARTITION BY nick_name
ORDER BY score DESC) as 2th
from user_info ui
join exam_record er on ui.uid = er.uid
join examination_info ei on er.exam_id = ei.exam_id
where score is not null
nfile():将分区中的有序数据分为n个桶,记录桶号。
此函数在数据分析中应用较多,比如由于数据量大,需要将数据平均分配到N个并行的进程分别计算,此时就可以用NFILE(N)对数据进行分组,由于记录数不一定被N整除,所以数据不一定完全平均,多出来的部分则依次加给第一组、第二组···直到分配完。