結合實例來分析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:窗口函數和普通函數的區別在於:普通聚合函數結果返回的是一條,將多條記錄合成一條,而窗口函數是有幾條記錄就返回幾條。

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