敲黑板啦!開窗函數你學會了嗎

什麼是開窗函數?

開窗函數用於爲行定義一個窗口(這裏的窗口是指運算將要操作的行的集合),它對一組值進行操作,不需要使用GROUP BY子句對數據進行分組,能夠在同一行中同時返回基礎行的列和聚合列。

  1. Window Function又稱爲窗口函數、分析函數。
  2. 窗口函數與聚合函數類似,但是每一行數據都生成一個結果。
  3. 聚合函數(比如sum、avg、max等)可以將多行數據按照規定聚合爲一行,一般來講聚集後的行數要少於聚集前的行數。但是有時我們想要既顯示聚集前的數據,又要顯示聚集後的數據,這時便引入了窗口函數。
  4. 窗口函數是在select時執行的,位於order by之前。

OVER(PARTITION BY)
over不能單獨使用,要和分析函數:rank(),dense_rank(),row_number()等一起使用。
其參數:over(partition by columnname1 order by columnname2)
含義:按columname1指定的字段進行分組排序,或者說按字段columnname1的值進行分組排序。 開窗函數over( ),包含三個分析子句:partition by , order by , rows rows代表開窗,窗口就是分析函數要處理的範圍

學習目標:

1、掌握sum()avg()用於累計計算的函數
2、掌握row_number()rank()用於排序的函數
3、掌握ntile()用於分組查詢的函數
4、掌握lag()lead()``偏移分析函數

1、累計計算窗口函數

(1)sum(…) over(……)

大家在做報表的時候,經常會遇到計算截止某月的累計數值,通常在
EXCEL裏可以通過函數來實現。
在這裏插入圖片描述

那麼在Hive中,我們該如何實現這種累計數值的計算呢?——利用窗口函數
我們通過幾個需求案例來講解開窗函數的具體應用。

需求1-1:
對2018年公司的支付總額按月度累計進行分析。
按月度進行累計分析:

SELECT a.month,
	a.pay_amount,
	sum(a.pay_amount) over(order by a.month)
FROM
	(SELECT month(dt) month,
		sum(pay_amount) pay_amount
	FROM user_trade
	WHERE year(dt)=2018
	GROUP BY month(dt))a;

SQL解析:
子查詢中:dt 是分區字段,這裏month(dt)是取月份,需求要按月度進行分析,所以我們要按month分組GROUP BY month(dt),還要計算每個月的支付總額sum(pay_amount),最後將子查詢結果命名爲a,子查詢要查的是兩個指標month和pay_amount
外層查詢我們要使用窗口函數了
查詢月份,以及每個月的支付總額,還有一個字段就是需求要求的支付總額按月度累計的字段。
sum(a.pay_amount) over(order by a.month)
這裏因爲要求累計,所以窗口函數這裏用到了sum函數,sum(a.pay_amount)是對子查詢結果a中的支付總額進行求和,這裏over(order by a.month)就是要根據月份month逐漸遞增累計求和。
在這裏插入圖片描述
例如:累計求和中第三月的累計月度支付總額5640670.2=第一月317697.2的支付總額 + 第二月2532234.3000000003的支付總額。
4月累計總額=前三個月累計總額+四月支付總額。

需求1-2:
對2017和2018年公司的支付總額按月度累計進行分析,按年度進行彙總。
分析:按照月度累計後,還要按照年度進行彙總,也就是2017年的支付總額和2018年的支付總額按年度累計分開顯示。

SELECT a.year year,
	a.month month,
	a.pay_amount pay_amount,
	sum(a.pay_amount) over(partition by a.year order by sum_pay_amount
a.month )
FROM
	(SELECT year(dt) year,
		month(dt) month,
		sum(pay_amount) pay_amount
	FROM user_trade
	WHERE year(dt) in (2017,2018)
	GROUP BY year(dt),
		month(dt))a;

SQL解析:
子查詢中:按照年和月進行分組GROUP BY year(dt),month(dt),年份選擇year(dt) in (2017,2018),要查詢的是年,月和支付在總額。
外層查詢:
前三列查詢年,月,支付總額,第四列就是開窗函數,與需求1-1相比多了個partition by a.year,也就是要根據year進行分組/分區,目的就是將2017年的數據和2018年的數據分兩個去彙總展示。
下圖中可以看到,2017年和2018年分兩個區展示累計月度支付總額。
在這裏插入圖片描述
說明:
1、partition by起到了分組的作用
2、order by 按照什麼順序進行累加,升序ASC、降序DESC,默認升序

(2)avg(…) over(……)

移動平均值
大家看股票的時候,經常會看到這種K線圖吧,裏面經常用到的就是7日、
30日移動平均的趨勢圖,那如何使用窗口函數來計算移動平均值呢?
在這裏插入圖片描述
對移動平均值的理解:
在這裏插入圖片描述
需求2:
對2018年每個月的近三個月進行移動地求平均支付金額。

SELECT
	a.MONTH MONTH,
	a.pay_amount pay_amount,
	avg( a.pay_amount ) over ( ORDER BY a.MONTH rows BETWEEN 2 preceding AND current ROW ) avg_pay_amount 
FROM
	(
	SELECT MONTH( dt ) MONTH,
		sum( pay_amount ) pay_amount 
	FROM
		user_trade 
	WHERE
		YEAR ( dt )= 2018 
GROUP BY
	MONTH ( dt )) a;

SQL解析:
子查詢中:查詢2018年的月份和支付金額,按照月份分組,給子查詢命名爲a
外層查詢中:查詢a表中的month,pay_mount,以及用到的窗口函數,計算近三個月進行移動地求平均支付金額。
因爲計算的平均值,所以用到了avg函數。
在這裏插入圖片描述
我們列舉4月份的數據看下:
4月的移動平均數是2680151.8666666667,它是怎麼來的呢?2680151.8666666667=(2214537.1+3108435.9000000004+2717482.6)/3

SQL中的開窗函數:avg( a.pay_amount ) over ( ORDER BY a.MONTH rows BETWEEN 2 preceding AND current ROW )是什麼意思呢?
答:根據month逐漸遞增計算移動平均數,這裏每個月的移動平均數等於前兩個月的支付總額加本月的支付總額除以三。
說明:
我們用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行)
unbounded preceding——之前所有的行
current row——本行
3 preceding——前三行
unbounded following——之後所有的行
1 following——下一行

拓展:
max(……) over(partition by …… order by …… rows between …… and ……)
min(……) over(partition by …… order by …… rows between …… and ……)

2、分區排序窗口函數

row_number() over(……)
rank() over(……)
dense_rank() over(……)
這三個函數的作用都是返回相應規則的排序序號

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:排序的字段名稱

注意:
row_number()的這個括號內是不加任何字段名稱的,rank() 和dense_rank() 同理。
row_number:它會爲查詢出來的每一行記錄生成一個序號,依次排序且不會重複。
rank&dense_rank:dense_rank函數出現相同排名時,將不跳過相同排名號,rank值緊接上一次的rank值。在各個分組內,rank()是跳躍排序,有兩個第一名時接下來就是第三名,dense_rank()是連續排序,有兩個第一名時仍然跟着第二名。

我們通過一個需求來理解一下分區排序窗口函數:
需求3:
對2019年1月份用戶的購買愛好進行分析。
我們要查不同用戶購買商品的愛好,我們可以查詢用戶購買商品品類數量的排名,以此來進行的分析用戶的愛好。

SELECT
	user_name,
	count( DISTINCT goods_category ) goods_num,
	row_number () over (ORDER BY count( DISTINCT goods_category)) row_number,
	rank () over (ORDER BY count( DISTINCT goods_category )) rank,
	dense_rank () over (ORDER BY count( DISTINCT goods_category)) dense_rank 
FROM
	user_trade 
WHERE
	substr( dt, 1, 7 )= '2019-01' 
GROUP BY
	user_name;

SQL解析:
substr 是字符串截取。我們要查不同用戶,所以要根據用戶進行分組。
查詢用戶名user_name,商品種類數goods_num,以及對應商品種類的窗口排序。
這裏用到分區排序窗口函數row_number (),rank ()和dense_rank (),還有我們要根據商品種類遞增進行分組後再排序。
我們通過查詢結果看下具體分區排序窗口函數的作用:
在這裏插入圖片描述
在查詢結果中我們可以看到:

  • 當商品種類是兩類時,row_number是依次排序並且不會重複;
  • 當商品種類是1類的時候,rank 的排序序號都是1,但如果是兩類商品時,rank就變爲了序號16,因爲rank()是跳躍排序,當遇到不同類時,它的排序會根據實際多少排名的下一名開始排序,也就是有兩個第一名時接下來就是第三名;
  • dense_rank 和rank一樣相同種類時他的排名不會發生改變,但是當商品種類爲兩類時,dense_rank會跟着前面的排名向下繼續排序,也就是有兩個第一名時仍然跟着第二名。

需求4:
選出2019年支付金額排名在第10、20、30名的用戶。

SELECT
	a.user_name,
	a.pay_amount,
	a.dr 
FROM
	(
	SELECT
		user_name,
		sum( pay_amount ) pay_amount,
		dense_rank () over ( ORDER BY sum( pay_amount ) DESC ) dr 
	FROM
		user_trade 
	WHERE
		YEAR ( dt )= 2019 
	GROUP BY
		user_name 
	) a 
WHERE
	a.dr IN ( 10, 20, 30 );

3、分組排序窗口函數

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)
如果切片不均勻,默認增加到第一個切片的分佈

需求5:
將2019年1月的支付用戶,按照支付金額分成5組。

SELECT user_name,
	sum(pay_amount) pay_amount,
	ntile(5) over(order by sum(pay_amount) desc) level
FROM user_trade
WHERE substr(dt,1,7)='2019-01'
GROUP BY user_name;

SQL解析:
ntile(5) 分成5組,按照總的支付金額進行降序排序。
在這裏插入圖片描述
結果分成5組,就是5個等級,第一級有4個人,第二級也有4個人…結果返回的是他們的等級。這裏因爲分成5組剛好很均勻,我們將切片改爲6看看結果:

SELECT user_name,
	sum(pay_amount) pay_amount,
	ntile(6) over(order by sum(pay_amount) desc) level
FROM user_trade
WHERE substr(dt,1,7)='2019-01'
GROUP BY user_name;

在這裏插入圖片描述
如果是ntile(6)的話,結果每個等級個數就被分成了443333

需求6:
選出2019年退款金額排名前10%的用戶。

SELECT
	a.user_name user_name,
	a.refund_amount refund_amount,
	a.level level
FROM
	(SELECT user_name,
		sum(refund_amount) refund_amount,
		ntile(10) over(order by sum(refund_amount)desc) level
	FROM user_refund
	WHERE year(dt)=2019
	GROUP BY user_name)a
WHERE a.level=1; 

SQL解析:
子查詢中:我們查詢退款總額,以及退款金額前10%並且根據退款總金額的降序排序的用戶。
refund_piece——退款件數
refund_amount ——退款金額
ntile(10) over(order by sum(refund_amount)desc) level 這裏ntile(10)就是代表10%,分成10組每組就是10%
在這裏插入圖片描述

4、偏移分析窗口函數

LagLead分析函數可以在同一次查詢中取出同一字段的前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那麼在數學運算中,總要給一個默認值纔不會出錯

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');

在這裏插入圖片描述
我們以Alexander爲例來看下:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
關鍵是看有沒有偏移量,有沒有默認值。

lead()就是取後幾行的數值。
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');

在這裏插入圖片描述

練習

做點練習吧!!!

需求7:
支付時間間隔超過100天的用戶數【跨時間潛在VIP用戶流失分析】

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

SQL解析:
計算支付時間間隔我們用lead_dt和dt相差時間來算, datediff是用來計算時間間隔。

需求8:
每個城市,不同性別,2018年支付金額最高的TOP3用戶。

SELECT c.user_name,
    c.city,
    c.sex,
    c.pay_amount,
    c.rank
FROM
    (SELECT a.user_name,
            b.city,
            b.sex,
            a.pay_amount,
row_number() over(partition by b.city,b.sex order by a.pay_amount desc) rank
FROM
    (SELECT user_name,
            sum(pay_amount) pay_amount
    FROM user_trade
    WHERE year(dt)=2018
    GROUP BY user_name)a
    LEFT JOIN user_info b
    on a.user_name=b.user_name)c
WHERE c.rank<=3;

SQL解析:
我們看sql需要從裏向外看,分析需求也是需要我們一個一個往出扣指標。
子查詢c表中,我們查出了用戶名user_name,支付金額pay_amount,並且我們通過連接用戶信息表user_info 可以查出城市,性別這些信息。
row_number() over(partition by b.city,b.sex order by a.pay_amount desc) rank
這句的意思是我們依據row_number()進行排序,對城市和性別分組後的值進行排序,支付金額降序排列。
在這裏插入圖片描述

需求9:
每個手機品牌退款金額前25%的用戶【跨手機品牌用戶退款分析】

SELECT *
FROM
    (SELECT a.user_name,
        extra2['phonebrand'] as phonebrand,
        a.refund_amount,
        ntile(4) over(partition by extra2['phonebrand'] order by a.refund_amount desc) level
    FROM
        (SELECT user_name,
            sum(refund_amount) refund_amount
        FROM user_refund
        WHERE dt>'0'
        GROUP BY user_name)a
        LEFT JOIN user_info b
        on a.user_name=b.user_name)c
WHERE c.level=1;

SQL解析:
ntile(4) over(partition by extra2[‘phonebrand’] order by a.refund_amount desc) level
這句話的意思是:將結果分爲4組,計算25%即ntile(4)
根據 extra2[‘phonebrand’] 進行分組,退款金額refund_amount 降序排列
dt>‘0’ 是查看所有分區
dt 是一個分區字段,extra2存放的是map類型的數據,根據phonebrand找出對應手機品牌

在這裏插入圖片描述

總結:

1、注意如何對sum()、avg()這類累計計算的窗口函數的行數限制。
2、不要混淆row_number()、rank()、dense_rank()三種函數。
3、會使用ntile()進行分組查詢。
4、lag():前N行、lead():後N行。
5、總結Hive中窗口函數的用法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章