窗口函數介紹
窗口函數語法
<窗口函數> over (partition by <用於分組的列名>
order by <用於排序的列名>)
- 專用窗口函數,比如rank, dense_rank, row_number等
- 聚合函數,如sum. avg, count, max, min等
窗口函數功能
- 不減少原表的行數,所以經常用來在每組內排名
- 同時具有分組(partition by)和排序(order by)的功能
窗口函數使用場景
業務需求“在每組內排名”,比如:
- 排名問題:每個部門按業績來排名
- topN問題:找出每個部門排名前N的員工進行獎勵
注意事項
- 窗口函數原則上只能寫在select子句中
- partition子句可以省略,省略就是不指定分組,但是,這就失去了窗口函數的功能,所以一般不要這么使用。
group by、order by子句與窗口函數的區別
group by分組匯總后改變了表的行數,一行只有一個類別。而partiition by和rank函數不會減少原表中的行數
標准聚合函數
標准的聚合函數有avg、count、sum、max和min,接下來分別介紹這些聚合函數的窗口函數形式。
移動平均窗口函數
移動平均值的定義:若依次得到測定值(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
例子
SELECT *,AVG(grade) OVER(
ORDER BY stu_no
ROWS BETWEEN 2 preceding AND CURRENT ROW
) AS '三移動平均'
FROM v_info
計數(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
例子2
按照課程號進行分組,找出成績大於等於80分的學生人數
SELECT *,
COUNT(*) OVER(PARTITION BY c_no) AS 'ct'
FROM v_info
WHERE grade>=80
小聲bb:這兩個例子舉得不是很好,如果只是為了看學生人數,用group by能更明了地看。應該說找出大於等於80分的學生人數及其相關信息。
累計求和(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
最大(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
求成績的累積最大值和累積最小值
例子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
例子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
排序窗口函數
row_number()、rank()、dense_rank(),這三個函數的作用都是返回相應規則的排序序號。
row_number()
為查詢出來的每一行記錄都會生成一個序號,依次排序且不會重復。
語法
row_number() over(partition by 字段1 order by 字段2) # 字段1是分組的字段名稱
rank()
使用rank函數來生成序號,over子句中排序字段值相同的序號是一樣的,后面字段值不相同的序號將跳過相同的排名排下一個,rank函數生成的序號有可能是不連續的,即排名可能為1,1,3,是跳躍式排名,有兩個第一名時接下來就是第三名。
語法
rank() over(partition by 字段1 order by 字段2)
dense_rank()
dense_rank函數在生成序號時是連續的,當出現相同排名時,將不跳過相同排名號,有兩個第一名時仍跟着第二名,即排名為1,1,2這種。
語法
dense_rank() over(partition by 字段1 order by 字段2)
分組排序窗口函數
可以按照銷售額的高低、點擊次數的高低,以及成績的高低為對用戶和學生進行分組,這里的考點是:取銷售額最高的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
例子2
向后推1個日期
SELECT *,
LEAD(birth,1,'無') OVER(PARTITION BY sex) AS 'lead_1'
FROM v_info
例子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