数据分析课程笔记 - 20 - HIVE 核心技能之窗口函数

大家好呀,这节课我们学习 Hive 核心技能中最难的部分——窗口函数。窗口函数我们之前在学 MySQL 的时候有学过一些,但是只学了三个排序的窗口函数。这节课我们会学习更多的窗口函数,包括累计计算、分区排序、切片排序以及偏移分析。

在正式学习之前,我们需要先明确一下窗口函数和GROUP BY分组的区别。二者在功能上有相似之处,但是它们存在本质区别。

1. 分组会改变表的结构,而窗口函数不会改变表的结构。比如原表有10行数据,分成两组后只有两行,而窗口函数仍然返回十行数据。
2. 分组只能查询分组后的字段,包括分组字段(组名)和聚合函数字段。而窗口函数对查询字段没有限制,也就是可以查询原表的任意字段,再加上窗口函数新增的一列值。

好啦,现在让我们一起进入窗口函数的世界吧~

本节课主要内容:

1、累计计算窗口函数
(1)sum(…) over(……)
(2)avg(…) over(……)
(3)语法总结
2、分区排序窗口函数
(1)row_number()
(2)rank()
(3)dense_rank()
3、切片排序窗口函数
(1)ntile(n) over(……)
4、偏移分析窗口函数
5、重点练习

一、累计计算窗口函数

1、sum(…) over(……)

大家在做报表的时候,经常会遇到计算截止某月的累计数值,通常在EXCEL里可以通过函数来实现。

image.png

那么在HiveSQL里,该如何实现这种累计数值的计算呢?那就是利用窗口函数!

关于窗口函数的几点说明:

  1. Window Function又称为窗口函数、分析函数。
  2. 窗口函数与聚合函数类似,但是它是每一行数据都生成一个结果。
  3. 聚合函数(比如sum、avg、max等)可以将多行数据按照规定聚合为一行,一般来讲聚集后的行数要少于聚集前的行数。但是有时我们想要既显示聚集前的数据,又要显示聚集后的数据,就可以通过窗口函数实现。
  4. 窗口函数是在select时执行的,位于order by之前。

需求1:对2018年公司的支付总额按月度累计进行分析

需求分析:既然要进行按月累计,我们就先要把2018年的每笔交易时间转换成月并按月分组聚合计算,得出一个2018年每月支付金额总合表,再基于这张表用窗口函数进行累计计算。

2018年每月支付金额总和表:

SELECT month(trade_time) as month,
    sum(amount) as total_amount
FROM trade_2018
GROUP BY month(trade_time);
2018年每月支付金额总和

再用窗口函数进行月度累计:

SELECT a.month,
    a.total_amount,
    sum(a.total_amount) over(order by a.month) as sum_total_amount
FROM
    (SELECT month(dt) as month,
        sum(amount) as total_amount
    FROM user_trade
    WHERE year(dt) = 2018
    GROUP BY month(dt)) a;
需求1结果

需求2:对2017和2018年公司的支付总额按月度累计进行分析,按

年度进行汇总。

这个需求比需求1多了一个需求,那就是年度汇总。那我们只需要在上个需求的子查询中加一个 year 字段即可。

SELECT a.year,
    a.month,
    a.total_amount,
    sum(a.total_amount) over(partition by a.year order by a.month) as sum_total_amount
FROM
    (SELECT month(dt) as month,
        year(dt) as year,
        sum(pay_amount) as total_amount
    FROM user_trade
    WHERE year(dt) in (2017,2018)
    GROUP BY month(dt), year(dt)) a;
需求2结果

说明:

1、over 中的 partition by 起到了窗口内将数据分组的作用。事实上,加上partition by之后,可以理解为分成了多个窗口,并在每个窗口内进行累加计算或者分区。

如果不加 partition by a.year 的话,运行结果就是这样单纯按月份进行分组的:

image.png

2、order by 按照什么顺序进行累加,升序ASC、降序DESC,默认是升序。

2、avg(…) over(……)

大家看股票的时候,经常会看到这种K线图,里面经常用到的就是7日、30日移动平均的趋势图,那如何使用窗口函数来计算移动平均值呢?

K线图

需求3:对2018年每个月的近三个月进行移动地求平均支付金额

需求分析:这个需求要求每个月近三个月的移动平均支付金额,这里我们要用到一个新知识点,在窗口函数 avg over 的 order by a.month 之后加一句 rows between 2 preceding and current row 来设定计算移动平均的范围,这个语句的含义就是包含本行及前两行。其他部分的写法跟前面的需求类似,先取出2018年每个月的支付金额总和,再用窗口函数求移动平均。

SELECT a.month,
    a.total_amount,
    avg(a.total_amount) over(order by a.month rows between 2 preceding and current row) as avg_total_amount
FROM
    (SELECT month(dt) as month,
        sum(pay_amount) as total_amount
    FROM user_trade
    WHERE year(dt)=2018
    GROUP by month(dt)) a;
需求3结果

注意:

  1. 1月和2月分别是它本身和前两个月的平均。因为整体范围限定在2018年的数据。
  2. 如果不加 rows between 2 preceding and current row,那么出来的结果是对每一行前面所有行进行平均,比如第四行后面返回的值就是前四行的平均值。

3、语法总结

sum(…A…) over(partition by …B… order by …C… rows between …D1… and …D2…)
avg(…A…) over(partition by …B… order by …C… rows between…D1… and …D2…)

A:需要被加工的字段名称
B:分组的字段名称
C:排序的字段名称
D:计算的行数范围

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
——从前三行到下一行(5行)

窗口函数结合其他聚合函数

max(……) over(partition by …… order by …… rows between ……and ……)

min(……) over(partition by …… order by …… rows between ……and ……)

二、分区排序窗口函数

row_number() 、rank()、dense_rank()

用法:这三个函数的作用都是返回相应规则的排序序号

row_number() over(partition by …A… order by …B… )

rank() over(partition by …A… order by …B… )

dense_rank() over(partition by …A… order by …B… )

A:分组的字段名称

B:排序的字段名称

注意: 这3个函数的括号内是不加任何字段名称的!

row_number:它会为查询出来的每一行记录生成一个序号,依次排序且不会重复。

rank&dense_rank:在各个分组内,rank() 是跳跃排序,有两个第一名时接下来就是第三名,dense_rank() 是连续排序,有两个第一名时仍然跟着第二名。

实例练习:

需求4:2019年1月,用户购买商品品类数量的排名

再眼熟一下 user_trade 的表结构:

user_trade表结构

需求分析:先限定时间范围,然后根据 user_name 进行分组,接着选出 分组去重后的 user_name,并计算每个用户 goods_category 的数量(记得 distinct 去重),再然后就是用窗口函数对 goods_category 的数量进行排序,当然选择哪一种排序方法要看具体要求,这里我们可以三种方法都试一下看看结果:

SELECT user_name,
    count(DISTINCT goods_category) as goods_num,
    row_number() over(order by count(DISTINCT goods_category) DESC) as row_number,
    rank() over(order by count(DISTINCT goods_category) DESC) as rank,
    dense_rank() over (order by count(DISTINCT goods_category) DESC) as dense_rank
FROM user_trade
WHERE substr(dt,1,7)='2019-01'
GROUP BY user_name;
需求4结果

注意:窗口函数中的 order by 字段不能用 select 中字段的重命名,因为二者是同时执行的。

需求5:选出2019年支付金额排名在第10、20、30名的用户

需求分析: 先用窗口函数将2019年每个用户的支付总金额算出来并进行排序,再以此作为子查询,从中取出排名在第10、20、30名的用户名、支付总金额以及排名次序。企业一般会使用 dense_rank 进行排序,所以我们这里直接用 dense_rank。

2019年每个用户的支付总金额排名:

SELECT user_name,
    sum(pay_amount) as total_amount,
    dense_rank() over(order by sum(pay_amount) DESC) as rank
FROM user_trade
WHERE year(dt)=2019
GROUP BY user_name;
第一步结果

2019年支付金额排名在第10、20、30名的用户:

SELECT a.user_name,
    a.total_amount,
    a.rank
FROM
(SELECT user_name,
    sum(pay_amount) as total_amount,
    dense_rank() over(order by sum(pay_amount) DESC) as rank
FROM user_trade
WHERE year(dt)=2019
GROUP BY user_name) a
WHERE rank in (10,20,30);
需求5结果

三、切片排序窗口函数

ntile(n) over(……)

ntile(n) over(partition by …A… order by …B… )

n:切分的片数
A:分组的字段名称
B:排序的字段名称

  • NTILE(n):用于将分组数据按照顺序切分成n片,返回当前切片值。
  • NTILE不支持ROWS BETWEEN,比如 NTILE(2) OVER(PARTITION BY …… ORDER BY …… ROWS BETWEEN 3 PRECEDING AND CURRENT ROW)。
  • 如果切片不均匀,则前面的组分得的数据多。

需求6:将2019年1月的支付用户,按照支付金额分成5组

需求分析:这个需求很简单,把需求5第一步的排序窗口函数变成切片即可。注意时间筛选条件变成2019年1月。

SELECT user_name,
    sum(pay_amount) as total_amount,
    ntile(5) over(order by sum(pay_amount) DESC) as level
FROM user_trade
WHERE substr(dt,1,7)='2019-01'
GROUP BY user_name;
需求6结果

需求7:选出2019年退款金额排名前10%的用户

需求分析: 排名前10%,也就是一共分成10组,取第1组。那么我们先切片分组:

SELECT user_name,
    sum(refund_amount) as total_refund_amount,
    ntile(10) over(order by sum(refund_amount) DESC) as level
FROM user_refund
WHERE year(dt)=2019
GROUP BY user_name;
分成10组

然后再取第一组:

SELECT a.user_name,
    a.total_refund_amount,
    a.level
FROM
    (SELECT user_name,
        sum(refund_amount) as total_refund_amount,
        ntile(10) over(order by sum(refund_amount) DESC) as level
    FROM user_refund
    WHERE year(dt)=2019
    GROUP BY user_name) a
WHERE level = 1;
需求7结果

四、偏移分析窗口函数

说明:Lag和Lead分析函数可以在同一次查询中取出同一字段的前N行的数据(Lag)和后N行的数据(Lead)作为独立的列。

在实际应用当中,若要用到取今天和昨天的某字段差值时,Lag和Lead函数的应用就显得尤为重要。当然,这种操作可以用表的自连接实现,但是LAG和LEAD与left join、right join等自连接相比,效率更高,SQL语句更简洁。

lag(exp_str,offset,defval) over(partion by ……order by ……)
lead(exp_str,offset,defval) over(partion by ……order by ……)

  • exp_str 是字段名称。
  • offset 表示偏移量,即是上1个或上N个的值,假设当前行在表中排在第5行,则 offset 为3,则表示我们所要找的数据行就是表中的第2行(即5-3=2)。offset默认值为1。
  • defval 默认值,当这两个函数取上N/下N个值时,在表中从当前行位置向前数N行已经超出了表的范围时,lag()函数将defval这个参数值作为函数的返回值,若没有指定默认值,则返回NULL。lead()函数也是一样的道理。那么在数学运算中,总要给一个默认值才不会出错。

lag() 函数示例:

-- Alice和Alexander的各种向上时间偏移
SELECT user_name,
    dt,
    lag(dt,1,dt) over(partition by user_name order by dt),
    lag(dt) over(partition by user_name order by dt),
    lag(dt,2,dt) over(partition by user_name order by dt),
    lag(dt,2) over(partition by user_name order by dt)
FROM user_trade
WHERE dt>'0'
    and user_name in ('Alice','Alexander');
image.png

lead() 函数示例:

-- Alice和Alexander的各种向下时间偏移
SELECT user_name,
    dt,
    lead(dt,1,dt) over(partition by user_name order by dt),
    lead(dt) over(partition by user_name order by dt),
    lead(dt,2,dt) over(partition by user_name order by dt),
    lead(dt,2) over(partition by user_name order by dt)
FROM user_trade
WHERE dt>'0'
    and user_name in ('Alice','Alexander');
image.png

需求8:支付时间间隔超过100天的用户数

需求分析:先要从 user_trade 表中取出每个用户的支付时间,把每个用户放到一个窗口中,按照支付时间进行排序,取出偏移列: lead(dt,1,dt) over(partition by user_name order by dt)。接着基于该子查询,筛选出时间间隔大于100天的用户,并计算数量。

SELECT count(distinct a.user_name)
FROM
    (SELECT user_name,
        dt,
        lead(dt) over(partition by user_name order by dt) as lead_time
    FROM user_trade
    WHERE dt is not null) a
WHERE datediff(a.lead_time, a.dt) > 100;

注意: 如果上面偏移分析函数写成 lead(dt,1,dt) 就不用加后面的 dt is not null 了,因为有默认值的话,间隔就是0,肯定是不满足条件的。

五、重点练习

需求9:每个城市,不同性别,2018年支付金额最高的TOP3用户

需求分析

第一步:这个需求要用到 user_trade 和 user_info 两张表,前者取支付时间和金额,后者取城市和性别。先对这两张表基于 user_name 进行左连接,并取出相应字段,用窗口函数进行分组排序:

SELECT a.user_name,
    b.city,
    b.sex,
    a.pay_amount,
    dense_rank() over(partition by b.city, b.sex order by a.pay_amount DESC) as dr
FROM
    (SELECT user_name,
    sum(pay_amount) as pay_amount
    FROM user_trade
    WHERE year(dt)=2018
    GROUP BY user_name) a
    LEFT JOIN
    (SELECT user_name,
        city,
        sex
    FROM user_info) b
    on a.user_name = b.user_name;

这一步的运行结果是这样的:

第一步结果

第二步:基于上述结果取出TOP3:

SELECT c.*
FROM
    (SELECT a.user_name,
        b.city,
        b.sex,
        a.pay_amount,
        dense_rank() over(partition by b.city, b.sex order by a.pay_amount DESC) as dr
    FROM
        (SELECT user_name,
        sum(pay_amount) as pay_amount
        FROM user_trade
        WHERE year(dt)=2018
        GROUP BY user_name) a
        LEFT JOIN
        (SELECT user_name,
            city,
            sex
        FROM user_info) b
        on a.user_name = b.user_name) c
WHERE c.dr in (1,2,3);
需求9最终结果

需求10:每个手机品牌退款金额前25%的用户

需求分析:

第一步:这个需求同样要用到两张表 user_refund 和 user_info。我们先把每个退款用户的退款金额和手机品牌取出来,并用窗口函数进行切片排序,25%就是分成4片:

SELECT a.user_name,
    b.phone_brand,
    a.refund_amount,
    ntile(4) over(partition by b.phone_brand order by a.refund_amount DESC) as nt
FROM
    (SELECT user_name,
    sum(refund_amount) as refund_amount
    FROM user_refund
    WHERE dt is not null
    GROUP BY user_name) a
    LEFT JOIN
    (SELECT user_name,
        get_json_object(extra1,'$.phonebrand') as phone_brand
    FROM user_info) b
    on a.user_name = b.user_name;

注意:这里之所以要加 WHERE dt is not null 是因为 user_refund 是一个分区表,分区表要对分区字段进行限制,否则 hive 会报错。

第一步结果

第二步:选择前25%,也就是第一片:

SELECT c.*
FROM
(SELECT a.user_name,
    b.phone_brand,
    a.refund_amount,
    ntile(4) over(partition by b.phone_brand order by a.refund_amount DESC) as nt
FROM
    (SELECT user_name,
    sum(refund_amount) as refund_amount
    FROM user_refund
    WHERE dt is not null
    GROUP BY user_name) a
    LEFT JOIN
    (SELECT user_name,
        get_json_object(extra1,'$.phonebrand') as phone_brand
    FROM user_info) b
    on a.user_name = b.user_name) c
WHERE c.nt = 1;
需求10最终结果

最后补充一个从 hive 导出结果数据的命令:

-- 结果导出语句
insert overwrite local directory '/home/kkb/result3' 
    row format delimited fields terminated by '\t'
    collection items terminated by ',' 
    map keys terminated by ':' 
    lines terminated by '\n' 
    select * from user_info limit 5;

以上就是这节课的全部内容了。做完整个练习,真的半条命都没了。窗口函数果然很难,不过掌握方法、多多练习,学会拆解需求,一步一步来做,就能明显降低难度。希望以后有机会能用到这么复杂的技能,哈哈~!

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

推荐阅读更多精彩内容