结合实例来分析SQL的窗口函数
Posted 随风
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了结合实例来分析SQL的窗口函数相关的知识,希望对你有一定的参考价值。
这篇主要是用举栗子的方式来理解SQL中的窗口函数,加深大家对SQL窗口函数的理解。
样例表这个样例表是我为了好理解,随便设计的,不符合数据库设计的三范式,请忽略。
(一)标准聚合函数
标准的聚合函数有avg、count、sum、max和min,接下来分别介绍这些聚合函数的窗口函数形式。
1、移动平均窗口函数
移动平均值的定义:若依次得到测定值(x1,x2,x3,...xn)时,按顺序取一定个数所做的全部算数平均值。例如(x1+x2+x3)/3,(x2+x3+x4)/3,(x3+x4+x5)/3,....就是移动平均值。其中,x可以是日或者月,以上的可以成为3日移动平均,或3月移动平均,常用于股票分析中。
语法结构:
avg(字段名) over(partition by 字段名 order by 字段名 asc/desc rows between A and B )
-- A和B是计算的行数范围
影响行数的范围(限定计算移动平均的范围):
rows between 2 preceding and current row # 取当前行和前面两行
rows between unbounded preceding and current row # 包括本行和之前所有的行
rows between current row and unbounded following # 包括本行和之后所有的行
rows between 3 preceding and current row # 包括本行和前面三行
rows between 3 preceding and 1 following # 从前面三行和下面一行,总共五行
当order by后面缺少窗口从句条件,窗口规范默认是rows between unbounded preceding and current row.
当order by和窗口从句都缺失, 窗口规范默认是 rows between unbounded preceding and unbounded following
以v_info举个例子吧
SELECT *,
AVG(grade) OVER(ORDER BY stu_no ROWS BETWEEN 2 preceding AND CURRENT ROW) AS \'三移动平均\'
FROM v_info
- 对于第一行来说,没有前面两行,所以值就为当前行的值
- 对于第二行来说,前面只有一行,所以三移动平均就为第一行和第二行的平均值
影响行数范围的语句在标准的聚合函数中都适用。
2、计数(count)窗口函数
窗口 函数 count(*) over() 对于查询返回的每一行,它返回了表中所有行的计数。
语法结构:
count(字段名1) over(partition by 字段名2 order by 字段名3 asc/desc)
(1)查询出成绩在90分以上的人数
SELECT *,
COUNT(*) OVER() AS \'ct\'
FROM v_info
WHERE grade>=90
这个结果说明,成绩大于90分的,有两位同学。
(2)按照课程号进行分组,找出成绩大于等于80分的学生人数
SELECT *,
COUNT(*) OVER(PARTITION BY c_no) AS \'ct\'
FROM v_info
WHERE grade>=80
从结果上可以看出,课程号为“0001”的学生人数有2名;课程号为“0002”的学生人数有2名;课程号为“0003”的学生有3名。
3、累计求和(sum)窗口函数
语法结构:
sum(字段名1) over(partition by 字段名2 order by 字段名3 asc/desc)
--按照字段1进行累积求和
-- 按照字段2 进行分组
-- 在组内按照字段3进行排序
(1)根据学号排序,对学生的成绩进行累积求和
SELECT *,
SUM(grade) OVER(ORDER BY stu_no) AS \'累积求和\'
FROM v_info
(2)按照课程号分组,然后根据学号对成绩进行累积求和
SELECT *,
SUM(grade) OVER(PARTITION BY c_no ORDER BY stu_no) AS \'累积求和\'
FROM v_info
tips:一定要选择根据学号排序,要不然得出来的是最终的累积求和结果,如下图:
SELECT *,
SUM(grade) OVER(PARTITION BY c_no) AS \'累积求和\'
FROM v_info
4、最大(max)、最小值(min)窗口函数
语法结构:
max(字段名1) over(partition by 字段名2 order by 字段名3 asc/desc)
min(字段名1) over(partition by 字段名2 order by 字段名3 asc/desc)
(1)求成绩的累积最大值和累积最小值
SELECT *,
MAX(grade) OVER(ORDER BY stu_no) AS \'累积最大值\',
MIN(grade) OVER(ORDER BY stu_no) AS \'累积最小值\'
FROM v_info
按照学号进行排序,在累积最大值中,会依次往下找最大值,如果有比当前值大的,就更新,若没有就保持当前;最小值同理。
(2)按照课程号进行分组,再求最大、最小值
SELECT *,
MAX(grade) OVER(PARTITION BY c_no ORDER BY stu_no) AS \'累积最大值\',
MIN(grade) OVER(PARTITION BY c_no ORDER BY stu_no) AS \'累积最小值\'
FROM v_info
(3)根据学生号和课程号求成绩的累积最小值
SELECT stu_no,c_no,stu_name,sex,birth,grade,
MIN(grade) OVER(PARTITION BY stu_no,c_no) AS \'累积最小值\'
FROM v_info
从上图可以看出,对于stu_no,c_no分组,后面没有一样的分组,所以每个stu_no,c_no都是一组,所以累积最小值就是当前的成绩值。
(4)统计2019年10月1日-10月10日每天做新题的人的数量,重点在每天。
- 这个题的重点是在每天,所以需要求出count(时间)=10的用户ID;
- 这个题可以使用min() over()窗口函数,先根据每个做题者和试卷号,找出每个做题者的最小日期,这里和前面(3)的解题思路是一样的;
- 如果每天都做题,那么得到的日期是不一样的,所以count(时间)会等于10;
- 再对这部分的用户ID进行求和,就可以找出每天都做新题的人了。
SELECT COUNT(a.sno) AS \'每天做题的人数\'
FROM
(SELECT sno,
s_id,
time,
MIN(time) OVER(PARTITION BY sno,s_id) AS \'first_time\'
FROM paper
WHERE DATE_FORMAT(time,\'%Y-%m-%d\') BETWEEN \'2019-10-01\' AND \'2019-10-10\') AS a
WHERE a.time=a.first_time
GROUP BY a.sno
HAVING COUNT(DISTINCT a.first_time)=10
(二)排序窗口函数
我在之前就更新过了,这里就不重复写了,感兴趣的可以点链接,去看我之前写的文章。
(三)分组排序窗口函数
可以按照销售额的高低、点击次数的高低,以及成绩的高低为对用户和学生进行分组,这里的考点是:取销售额最高的25%的用户(将用户分成4组,取出第一组)、取成绩高的前10%的学生(将学生分成10组,取出第一组)等等。
语法结构:
ntile(n) over(partition by 字段名2 order by 字段名3 asc/desc)
--n表示要切分的片数,如需要取前25%的用户,则需要分为4组,取前10%的用户,则需要分10组
- ntile(n),用于将分组数据按照顺序切分成n片,返回当前切片值
- ntile不支持rows between的用法
- 切片如果不均匀,默认增加第一个切片的分布
(1)取出成绩前25%的学生信息
- 第一步:按照成绩的高低,将学生按照成绩进行切片
SELECT *,
ntile(4) OVER(ORDER BY grade DESC) AS \'rank\'
FROM v_info
- 第二步:按照rank筛选出第一组,则得到最终的结果如下:
SELECT a.*
FROM
(SELECT *,
ntile(4) OVER(ORDER BY grade DESC) AS \'rank\'
FROM v_info) AS a
WHERE a.rank=1
(四)偏移分析窗口函数
lag() over()和lead() over()窗口函数,lag和lead分析函数可以在同一次查询中取出同一个字段的前N行数据(lag)和后N行(lead)作为独立的列。
在实际应用当中,若要用到取今天和昨天的某字段的差值时,lag和lead函数的应用就显得尤为重要了。
适用场景:获取用户在某个页面停留的起始与结束时间
语法结构:
lag(exp_str,offset,defval) over(partition by ... order by...)
lead(exp_str,offset,defval) over(partition by ... order by...)
-- exp_str表示字段名称
-- offset偏移量,假设当前行在表中排在第5行,则offset为3,则表示我们所要找的数据行就是表中的第2行(即5-3=2)
-- offset默认为1
(1)向前推1个日期
SELECT *,
LAG(birth,1,0) OVER(PARTITION BY sex) AS \'lag_1\'
FROM v_info
- 第一条记录,往前推没有,则为0,因为我设置了为0,默认为NULL;
- 第四条记录是在男生组里,所以也相当于第一条记录,所以也为0;
(2)向后推1个日期
SELECT *,
LEAD(birth,1,\'无\') OVER(PARTITION BY sex) AS \'lead_1\'
FROM v_info
- 在女生组里,第三条记录往后推1个日期是没有的,所以为无;
- 在男生组里,最后一条记录网后也是没有的,所以也为无。
(3) 统计每天符合以下条件的用户数:A操作之后是B操作,AB操作必须相邻。
用户行为表racking_log(user_id,operate_id,log_time)
解题思路:
- 先根据用户ID和日期,用LEAD()窗口函数向后获取下一步的步骤;
- AB必须相邻,则表明当前的步骤为A,而下一个步骤为B,即A向下移的步骤是B;
- “每天”,即根据日期进行分组。
SELECT a.log_date,
COUNT(DISTINCT a.user_id)
FROM
(SELECT user_id,
operate_id,
DATE_FORMAT(log_time,\'%Y-%m-%d\') AS log_date,
LEAD(operate_id,1,NULL) OVER(PARTITION BY user_id,DATE_FORMAT(log_time,\'%Y-%m-%d\') ORDER BY log_time) AS \'next_operate\'
FROM tracking_log) AS a
WHERE a.operate_id=A AND b.next_operate=B
GROUP BY a.log_date
(4)现在有某个登录表,找出连续登录7天以上的用户(看SQL面试题一)
tips:窗口函数和普通函数的区别在于:普通聚合函数结果返回的是一条,将多条记录合成一条,而窗口函数是有几条记录就返回几条。
Hive分析函数系列文章
在笔者比较早期的面试经历中,面试官经常会出一些 SQL 题,而其中最常见的就是那些需要用 窗口函数 来解题的。
后来在笔者作为面试官面试候选人时,也是常常把窗口函数作为考察点。所以掌握好 SQL 窗口函数还是很必要的。
接下来我们按照顺序来介绍各种窗口函数。
一、组内排序函数:Rank/Dense_Rank/Row_Number
组内排序,我们常常用于取分组内排序前N/后N的记录,或先分组排序然后根据序号关联组内前一条或后一条记录。
Row_Number() over(partition by col1 order by col2):数字相同序号不重复,序号最大值等于总记录数;
Rank() over(partition by col1 order by col2):数字相同序号会重复,重复值后会跳过某些序号,序号最大值仍等于总记录数;
Dense_Rank() over(partition by col1 order by col2):数字相同序号会重复,重复值后不跳过某些序号,序号最大值会小于记录数。
Hive分析窗口函数(二) NTILE,ROW_NUMBER,RANK,DENSE_RANK
示例,假定有如下一张表 test,执行下述 SQL 输出结果:
ID | Name | Sal |
---|---|---|
1 | a | 10 |
2 | a | 12 |
3 | b | 13 |
4 | b | 12 |
5 | a | 14 |
6 | a | 15 |
7 | a | 13 |
8 | b | 11 |
9 | a | 16 |
10 | b | 17 |
11 | a | 14 |
Select
name, sal,
row_number() over(partition by name orderby sal desc) rnk1,
rank() over(partition by name order by sal desc) rnk2,
dense_rank() over(partition by name order by sal desc) rnk3
From test
Name | Sal | rnk1 | rnk2 | rnk3 |
b | 17 | 1 | 1 | 1 |
b | 13 | 2 | 2 | 2 |
b | 12 | 3 | 3 | 3 |
b | 11 | 4 | 4 | 4 |
a | 16 | 1 | 1 | 1 |
a | 15 | 2 | 2 | 2 |
a | 14 | 3 | 3 | 3 |
a | 14 | 4 | 3 | 3 |
a | 13 | 5 | 5 | 4 |
a | 12 | 6 | 6 | 5 |
a | 10 | 7 | 7 | 6 |
二、累计计数、求和函数:SUM/AVG/MIN/MAX
这类函数应用非常广泛。包括不使用 group by 即可分组聚合、分组内滚动聚合从第一行到当前行或分组内滚动聚合前N行到后N行。
Hive分析窗口函数(一) SUM,AVG,MIN,MAX
以 sum 为例,它的函数公式为:sum() over(partition by col1 order by col2 rows between xxx and yyy)。
下面我们来看看这个例子,假设有如下日志表 log:
cookie1 2015-04-10 1
cookie1 2015-04-11 5
cookie1 2015-04-12 7
cookie1 2015-04-13 3
cookie1 2015-04-14 2
cookie1 2015-04-15 4
cookie1 2015-04-16 4
以下述代码来介绍各种使用场景:
select
cookieid, pv,
-- 从起点到当前行
sum(pv) over(partition by cookieid order by created) as pv1,
-- 从起点到当前行,结果同pv1
sum(pv) over(partition by cookieid order by created rows between unbounded preceding and current row) as pv2,
-- 分组内所有行
sum(pv) over(partition by cookieid) pv3,
-- 当前行往前3行 -> 当前行
sum(pv) over(partition by cookieid order by created rows between 3 preceding and current row) pv4,
-- 当前行往前3行 -> 当前行往后1行
sum(pv) over(partition by cookieid order by created rows between 3 preceding and 1 following) pv5,
-- 当前行往后所有行
sum(pv) over(partition by cookieid order by created rows between current row and unbounded following) as pv6
from log
运行结果如下:
使用说明:
- 如果不指定rows between,默认为从起点到当前行;
- 如果不指定order by,那么将分组内所有值累加;
- row between 子句,也叫window子句:
preceding 往前;
following 往后;
current row 当前行;
unbounded 起点/终点:UNBOUNDED PRECEDING:表示从前面的起点, UNBOUNDED FOLLOWING:表示到后面的终点。
4. Avg、Count、Max、Min 等函数与 Sum 用法一样。
三、分桶函数:Ntile
分桶可用于便捷选择前/后N分之几的数据。
将有序的数据集平均分配到指定数量的桶内,并将桶号分配给每一条记录。如果不能平均分配,则优先分配给较小编号的桶,并且各个桶中能放的行数最多差1。
如,取上述示例 test 表 Sal 排名前50%的订单记录:
Select *
From(
Select Id, Name, sal,
Ntile(2) over(partition by name order by sal desc) nt
From test
)t where t.nt=1
四、前后平移函数:Lag/Lead
前后平移可快捷计算同比、环比值;
Lag(col, n, DEFAULT) 用于统计窗口内往上第n行值;
Lead(col, n, DEFAULT) 用于统计窗口内往下第n行值, 与LAG相反。
小 tips: default省略,则默认为NULL; 不需显式进行手动分组排序,在使用函数 LAG/LEAD 时最终呈现记录为自动排序。
示例:
如某张有 doctorid, patientid, add_time 3个字段的表,下列 2段 SQL 运行结果分别如下:
select
doctorid, patientid, add_time,
Lag(patientid, 1) over(partition by doctorid order by add_time) patient_bf
from stats.bi.nestle_patient
select
doctorid, patientid, add_time,
Lead(patientid, 1, 5188786) over(partition by doctorid order by add_time) patient_af
from stats.bi.nestle_patient
五、分组取最早、最迟函数:First_Value/Last_Value
Hive分析窗口函数(四) LAG,LEAD,FIRST_VALUE,LAST_VALUE
分组取最早、最迟函数可用于取用户的首次行为时间、最后一次行为时间,计算生命周期。
First_Value():分组内排序后,获得组内当前行往前的首个值;
Last_Value():分组内排序后,获得组内当前行往前的最后一个值。
如某张有 doctorid, patientid, add_time 3个字段的表,下列 2段 SQL 运行结果分别如下:
select
doctorid, patientid, add_time,
First_Value(patientId) over(partition by doctorId order by add_time) first_patient,
Last_Value(patientId) over(partition by doctorId order by add_time) last_patient,
First_Value(patientId) over(partition by doctorId order by add_time desc) first_patient_desc
from stats.bi.nestle_patient
六、序列分析函数:CUME_DIST/ PERCENT_RANK
Hive分析窗口函数(三) CUME_DIST,PERCENT_RANK
Cume_Dist:小于等于当前值的行数/分组内总行数。应用场景统计,用于收入订单前多少的排名,或者订单数 pk 百分比。注意:没有 partition ,所有数据分到同一组。
Percent_Rank:分组内当前行的RANK值-1/分组内总行数-1。暂时没有想到应用场景。
SELECT
year,
doctorId,
orders,
CUME_DIST() OVER(ORDER BY orders) AS rn1,
CUME_DIST() OVER(PARTITION BY year ORDER BY orders desc) AS rn2
FROM(
select date_trunc('year', payTime) year, doctorId, count(distinct id) orders
from subscriptionorder
where paystatus=2
group by doctorId, date_trunc('year', payTime)
)
Hive分析窗口函数(五) GROUPING SETS,GROUPING__ID,CUBE,ROLLUPhttps://blog.csdn.net/weishuai90/article/details/128859054
Hive-基本操作入门https://blog.csdn.net/weishuai90/article/details/128753329
Hive函数大全–完整版(一)https://blog.csdn.net/weishuai90/article/details/128750886
Hive函数大全–完整版(二)https://blog.csdn.net/weishuai90/article/details/128751466
Hive函数大全–完整版(三)https://blog.csdn.net/weishuai90/article/details/128751468
参考:30分钟掌握 Hive SQL 优化(解决数据倾斜) - 知乎
以上是关于结合实例来分析SQL的窗口函数的主要内容,如果未能解决你的问题,请参考以下文章