SQL窗口函数及Pandas实现

写在前面
窗口函数在处理复杂需求时提供了一种更为简便的数据处理方式,在实际业务中应用非常广泛,也是面试官喜欢重点考察的知识点。

什么是窗口函数?

窗口函数也被称为联机分析函数(OLAP,Online Anallytical Processing)或者分析函数(Analytic Function),窗口指对满足条件的集合进行计算,并对每一行数据返回分析结果,窗口函数的格式如下:

<窗口函数> OVER (partition by <用于分组的列名> order by <用于排序的列名> frame_clause)

1、常用窗口函数

1) 聚合函数:sum()、count()、max()、min()、avg()   
2) 排序函数:row_number()、rank()、dense_rank()    
3) 分布函数:percent_rank()、cume_dist()
4) 平移函数:lead()、lag()
5) 首尾函数:first_val()、last_val()    

2、分区(partition by)

over中partition by类似group by对数据进行分区,此时,窗口函数会对每个分区单独进行分析,如果不指定partition by将会对整体数据进行分析。    

3、排序(order by)

over中的order by对分区內的数据进行排序,默认为升序,当order by某个字段中有重复值时会对重复值进行求和,然后对所有数据进行累加。

4、窗口大小(frame_clause)

over中的frame_clause指对分区集合指定一个移动窗口,当指定了窗口大小后函数就不会在分区上进行计算,而是基于窗口大小內的数据进行计算。窗口大小的格式如下:    

rows frame_start
or
rows between frame_start and frame_end

其中,rows表示偏移的行数。frame_start表示窗口的起始位置,有三种选项:

  • UNBOUNDED PRECEDING,为默认值,表示从第一行开始。
  • N PRECEDING,表示从前一行开始,前一行数据缺失则为0 。
  • CURRENT ROW,表示从当前行开始。

frame_end表示窗口的结束位置,有三种选项:

  • CURRENT ROW为默认值,表示从当前行结束。
  • N FOLLOWING,表示当前行后的第N行结束。
  • UNBOUNDED FOLLOWING,表示窗口到分区的最后一行结束。

sql中的默认选项为:rows between UNBOUNDED PRECEDING AND CURRENT ROW,表示统计从第一行至当前记录行。
rows between 1 PRECEDING AND 1 FOLLOWING,表示当前行和前一行及后面一行聚合,多用于近N月的数据统计。
rows between current row and UNBOUNDED FOLLOWING,表示当前行及后面所有行。

为什么要使用窗口函数

在实际业务中我们经常会遇到需要对数据结果进行额外的统计,例如在计算各部门员工薪资后新增一列为公司整体薪资,又或者对各部门薪资水平进行排序、计算占比等操作,此时如果不使用窗口函数可能需要对表进行多次的关联才能实现,因此使用窗口函数可以大大简化代码并提升代码的读写性能。

如何使用窗口函数

首先根据窗口函数的定义我们可以知道,窗口函数主要分为了聚合、排序、分布、平移及首尾等类型,对于每一种类型具体的应用场景如下:

聚合函数

聚合函数也可以充当窗口函数,我们需要经常对窗口下的数据集进行聚合统计,也是窗口函数中应用较为广泛的一类。例如,我们需要统计A公司各部门下各员工的销售,并对各部门的最大值、最小值、平均值及计数等。

sql实现

select dept, name, salary,
       sum(salary) over(partition by dept) as sum_salary, --各部门员工薪资求和
       avg(salary) over(partition by dept) as avg_salary, --各部门员工薪资求平均
       min(salary) over(partition by dept) as min_salary, --各部门员工薪资求最小
       max(salary) over(partition by dept) as max_salary  --各部门员工薪资求最大值
from data

python实现

import numpy as np
import pandas as pd
company=["A","B","C"]
data=pd.DataFrame({
    "dept":[company[x] for x in np.random.randint(0,len(company),8)],
    "name":["a","b","c","d","e","f","g","h"],
    "salary":np.random.randint(10,30,8)
}
)
data['sum_salary'] = data.groupby('dept')['salary'].transform('sum')
data['min_salary'] = data.groupby('dept')['salary'].transform('min')
data['mean_salary'] = data.groupby('dept')['salary'].transform('mean')
data['max_salary'] = data.groupby('dept')['salary'].transform('max')
data
1.png

排序函数

排序函数常用于对分组集或者整体数据进行排名,例如我们需要对各部门员工薪资进行排序,排序函数又可以根据排序方式有以下分类:

1) row_number:对分组內的数据进行"同分不同级"方式排序,不存在序号并列的现象,即使同分时排序也会不同。    
2) rank:对分组內的数据进行"同分同级且不紧密"方式排序,当同分时序号相同,其它排序按正常排名进行排序,即1,2,2,4,5。    
3) dense_rank:对分组內的数据进行"同分同级且紧密"方式排序,当同分时序号相同,其它排序按下一排名进行排序,即1,2,2,3,4。

sql实现

select dept, name, salary,
       row_number(salary) over(partition by dept order by salary desc) as row_number, --对各部门员工薪资按同分不同级方式排序
       rank(salary) over(partition by dept order by salary desc) as rank, --对各部门员工薪资按同分同级且紧密方式方式排序
       dense_rank(salary) over(partition by dept order by salary desc) as dense_rank --对各部门员工薪资按同分同级且不紧密方式方式排序
from data

python实现

import numpy as np
import pandas as pd
company=["A","B","C"]
data=pd.DataFrame({
    "dept":[company[x] for x in np.random.randint(0,len(company),8)],
    "name":["a","b","c","d","e","f","g","h"],
    "salary":np.random.randint(10,15,8)
}
)
data['row_number'] = data.groupby('dept')['salary'].rank(ascending=False,method='first') #同分不同级
data['rank'] = data.groupby('dept')['salary'].rank(ascending=False,method='min')   #"同分同级且不紧密"
data['dense_rank'] = data.groupby('dept')['salary'].rank(ascending=False,method='dense') #"同分同级且紧密"
data
2.png

分布函数

分布函数主要分为两类:percent_rank()和cume_dist()。
percent_rank():指按照排名计算百分比,即该排名位于区间[0,1]的位置,其中区间内第一名为值0,最后一名值为1。其具体公式为:
percent\_rank() = (rank - 1) / (rows - 1)
cume_dist():指区间內大于等于当前排名的行数占区间内总函数的比例。多用于判断比当前薪资、得分高的用户比例为多少。

sql实现

select dept, name, salary,
       percent_rank(salary) over(partition by dept order by salary desc) as percent_rank,
       cume_dist(salary) over(partition by dept order by salary desc) as cume_dist
from data
import numpy as np
import pandas as pd
company=["A","B","C"]
data=pd.DataFrame({
    "dept":[company[x] for x in np.random.randint(0,len(company),8)],
    "name":["a","b","c","d","e","f","g","h"],
    "salary":np.random.randint(10,15,8)
}
)
# data.groupby('dept')['salary'].rank(ascending=False,method='first',pct=True)
data['percent_rank'] = (data.groupby('dept')['salary'].rank(ascending=False,method='min')-1) / \
                            (data.groupby('dept')['salary'].transform('count')-1)  #如果分组只有一个记录则数据为na 
data['cume_dist'] = data.groupby('dept')['salary'].rank(ascending=False,method='first',pct=True)  #可以结合排序函数的方法使用
data
3.png

平移函数

分布函数主要分为两类:lead(列名,n)和lag(列名,n),此函数多用于计算指标同比、环比。
lead(列名,n):获取分区內向下平移n行数据。
lag(列名,n):获取分区內向上平移n行数据。

sql实现

select dept, name, salary,
       lead(salary,1) over(partition by dept order by salary desc ) as lead,
       lag(salary,1) over(partition by dept order by salary desc) as lag
from data
import numpy as np
import pandas as pd
company=["A","B","C"]
data=pd.DataFrame({
    "dept":[company[x] for x in np.random.randint(0,len(company),8)],
    "name":["a","b","c","d","e","f","g","h"],
    "salary":np.random.randint(10,30,8)
}
)
data['lead'] = data.sort_values(['dept','salary'],ascending=False).groupby('dept')['salary'].shift(-1) # 分区內向下平移一个单位
data['lag'] = data.sort_values(['dept','salary'],ascending=False).groupby('dept')['salary'].shift(1)  # 分区內向上平移一个单位
data
4.png

首尾函数

分布函数主要分为两类:first_val()和last_val()。
first_val():获取分区內第一行数据。
last_val():获取分区內最后一行数据。

sql实现

select dept, name, salary,
       first_val(salary) over(partition by dept order by salary desc ) as first_val,
       # 由于窗口函数默认的是第一行至当前行,所以在使用last_val()函数时,会出现分区内最后一行和当前行大小一致的情况,因此我们需要将分区偏移量改为第一行至最后一行。       
       last_val(salary) over(partition by dept order by salary desc rows between UNBOUNDED PRECEDING and UNBOUNDED FOLLOWING) as last_val
from data

python实现

import numpy as np
import pandas as pd
company=["A","B","C"]
data=pd.DataFrame({
    "dept":[company[x] for x in np.random.randint(0,len(company),8)],
    "name":["a","b","c","d","e","f","g","h"],
    "salary":np.random.randint(10,30,8)
}
)
data['first_val'] = data.groupby('dept')['salary'].transform('min')
data['last_val'] = data.groupby('dept')['salary'].transform('max')
data
5.png

Q&A

Q1:聚合函数和窗口函数的区别

区别:聚合函数是将多条数据聚合成一行数据,而窗口函数是为每一行数据返回一个结果。
联系:都是对一组数据进行分析,窗口函数可以使用聚合函数作为函数。
当需要对数据结果进行额外的统计时,我们常常需要使用窗口函数。

微信图片_20210302230821.png

Q2:SQL的执行顺序

SQL的书写顺序是SELECT、FROM、JOIN、ON、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT,其执行顺序见下图:

在这里我们需要强调一下sql的执行顺序,因为大多数情况下我们是不需要太多考虑sql执行顺序的,但是由于窗口函数的执行顺序位于大多数字段之后,只位于字段ORDER BY之前,因此相当于在执行所有字段生成的临时表基础之上执行的窗口函数的操作,举个例子:

从上图中我们可以看到,第二张图无GROUP BY和第三张有GROUP BY时,最终数据的行数由7条记录变为3条,就是因为窗口函数是基于GROUP BY 字段执行之后的临时表基础上进行的计算,因此在清楚SQL的执行顺序后我们就能很容易理解最后展现的结果了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • 一、窗口函数的使用场景 作为IT人士,日常工作中经常会遇到类似这样的需求: 医院看病,怎样知道上次就医距现在的时间...
    carter记录阅读 541评论 0 0
  • 参考: MySQL 8.0窗口函数:用非常规思维简易实现SQL需求 数分面试-SQL篇 一、mysql窗口函数简介...
    kaka22阅读 1,307评论 0 1
  • 一般的商业数据库(其实也就是DB2,Oracle,SQL Server)都具备窗口函数这个功能,只不过名称不同,我...
    花讽院_和狆阅读 1,525评论 2 1
  • 窗口表达式 窗口表达式允许应用开发者更容易地使用标准SQL命令构造复杂的在线分析处理(OLAP)查询。 例如,通过...
    李春田阅读 2,631评论 0 2
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,548评论 0 11