窗口函數在統計類的需求中很常見,稍微複雜一點的查詢需求就有可能用到它,使用窗口函數能夠極大的簡化咱們的 SQL 語句。像 Oracle、SQL Server 這些數據庫在較早的版本就支持窗口函數了,MySQL 直到 8.0 版本後才支持它。html
本文將介紹一些經常使用的窗口函數的用法。窗口函數按照實現方式分紅兩種:一種是非聚合窗口函數,另一種是聚合窗口函數。mysql
非聚合窗口函數是相對於聚合窗口函數來講的。聚合函數是對一組數據計算後返回單個值(即分組),非聚合函數一次只會處理一行數據。窗口聚合函數在行記錄上計算某個字段的結果時,可將窗口範圍內的數據輸入到聚合函數中,並不改變行數。算法
1 非聚合窗口函數
MySQL 支持的非聚合窗口函數見表1。sql
名稱 | 描述 |
---|---|
CUME_DIST() |
累積分配值 |
DENSE_RANK() |
當前行在其分區中的排名,稠密排序 |
FIRST_VALUE() |
指定區間範圍內的第一行的值 |
LAG() |
取排在當前行以前的值 |
LAST_VALUE() |
指定區間範圍內的最後一行的值 |
LEAD() |
取排在當前行以後的值 |
NTH_VALUE() |
指定區間範圍內第N行的值 |
NTILE() |
將數據分到N個桶,當前行所在的桶號 |
PERCENT_RANK() |
排名值的百分比 |
RANK() |
當前行在其分區中的排名,稀疏排序 |
ROW_NUMBER() |
分區內當前行的行號 |
<center>表1 非聚合窗口函數列表</center>數據庫
經常使用的函數有:ROW_NUMBER()
、RANK()
、DENSE_RANK()
、LEAD()
、LAG()
、NTH_VALUE()
、FIRST_VALUE()
、LAST_VALUE()
。微信
-
ROW_NUMBER()
、RANK()
、DENSE_RANK()
函數ROW_NUMBER()
、RANK()
、DENSE_RANK()
都是排序函數,均可以給區間內的數生成序號。若是區間內不存在重複值,它們的計算結果同樣。.net當出現重複值時,
ROW_NUMBER()
不考慮重複值,它會給相同的兩個值分配不一樣的編號,編號的範圍是從 1 到分區的行數。RANK()
和DENSE_RANK()
給重複的值生成相同的編號。不一樣的是,RANK()
生成的序號有間隙,即重複值的下一項的編號和重複值的編號並不連續(下一項的值的編號 $\neq$ 當前重複值項的編號+1),而DENSE_RANK()
就不是這樣。code這幾個函數的區別可結合下面的例子分析。htm
SELECT sal, ROW_NUMBER() OVER(ORDER BY sal) AS 'rn', RANK() OVER(ORDER BY sal) AS 'rk', DENSE_RANK() OVER(ORDER BY sal) AS 'dk' FROM emp; sal rn rk dk ------- ------ ------ -------- 800.00 1 1 1 950.00 2 2 2 1100.00 3 3 3 1250.00 4 4 4 1250.00 5 4 4 1300.00 6 6 5 1500.00 7 7 6 1600.00 8 8 7 2450.00 9 9 8 2850.00 10 10 9 2975.00 11 11 10 3000.00 12 12 11 3000.00 13 12 11 5000.00 14 14 12
若是沒有指定
partition by 分區字段
,那麼窗口函數操做的區間就是所有數據。咱們可讓函數只做用在 deptno 分區。SELECT sal, deptno, ROW_NUMBER() OVER( PARTITION BY deptno ORDER BY sal) AS 'rn' FROM emp; sal deptno rn ------- ------ -------- 1300.00 10 1 2450.00 10 2 5000.00 10 3 800.00 20 1 1100.00 20 2 2975.00 20 3 3000.00 20 4 3000.00 20 5 950.00 30 1 1250.00 30 2 1250.00 30 3 1500.00 30 4 1600.00 30 5 2850.00 30 6
若是咱們要獲取 emp 表中每一個部門工資最高的前兩名員工的信息,使用
ROW_NUMBER()
就能夠這麼寫。SELECT * FROM (SELECT empno, ename, deptno, sal, ROW_NUMBER() OVER( PARTITION BY deptno ORDER BY sal DESC ) AS rn FROM emp) t WHERE rn <= 2; empno ename deptno sal rn ------ ------ ------ ------- -------- 7839 KING 10 5000.00 1 7782 CLARK 10 2450.00 2 7788 SCOTT 20 3000.00 1 7902 FORD 20 3000.00 2 7698 BLAKE 30 2850.00 1 7499 ALLEN 30 1600.00 2
注意,若是在
OVER()
中沒有ORDER
子句,那麼,ROW_NUMBER()
生成的編號是不肯定的,而RANK()
、DENSE_RANK()
生成的編號都是 1 。 -
LAG()
、LEAD()
LAG()
能夠得到位於當前行以前的數據,若是指定了分區,則獲取數據的範圍只能在分區內。默認是獲取上一條的記錄,若是沒有獲取到,則返回 NULL。LAG()
的表達式是LAG(expr [, N[, default]]) [
null_treatment]
over_clause
,咱們能夠指定向後獲取第 N 行,以及在獲取不到數據時指定默認值。LEAD()
的表達式和LAG()
是同樣的,所以在LEAD()
中也能夠指定獲取的行數 N 及默認值。SELECT empno, sal, LAG(sal) OVER(ORDER BY empno)AS 'lag', LEAD(sal) OVER(ORDER BY empno)AS 'lead', LAG(sal,2,0) OVER(ORDER BY empno)AS 'lag_2' FROM emp LIMIT 6; empno sal lag lead lag_2 ------ ------- ------- ------- --------- 7369 800.00 (NULL) 1600.00 0.00 7499 1600.00 800.00 1250.00 0.00 7521 1250.00 1600.00 2975.00 800.00 7566 2975.00 1250.00 1250.00 1600.00 7654 1250.00 2975.00 2850.00 1250.00 7698 2850.00 1250.00 2450.00 2975.00
咱們使用
LAG(sal,2,0)
獲取當前行向前偏移 2 行的值,在當前行的編號是 1 和 2 時,因爲偏移的行不存在,只能返回默認值 0 。 -
FIRST_VALUE()
、LAST_VALUE()
、NTH_VALUE()
這幾個函數能夠分別獲取區間範圍內第一行、最後一行、第 N 行的值。
SELECT empno, deptno, FIRST_VALUE(empno) OVER( PARTITION BY deptno ORDER BY empno)AS 'first', LAST_VALUE(empno) OVER( PARTITION BY deptno ORDER BY empno ROWS BETWEEN unbounded preceding AND unbounded following)AS 'last', NTH_VALUE(empno,2) OVER( PARTITION BY deptno ORDER BY empno)AS 'nth_2' FROM emp; empno deptno first last nth_2 ------ ------ ------ ------ -------- 7782 10 7782 7934 (NULL) 7839 10 7782 7934 7839 7934 10 7782 7934 7839 7369 20 7369 7902 (NULL) 7566 20 7369 7902 7566 7788 20 7369 7902 7566 7876 20 7369 7902 7566 7902 20 7369 7902 7566 7499 30 7499 7900 (NULL) 7521 30 7499 7900 7521 7654 30 7499 7900 7521 7698 30 7499 7900 7521 7844 30 7499 7900 7521 7900 30 7499 7900 7521
當在
OVER()
中指定了排序字段,FIRST_VALUE()
、LAST_VALUE()
、NTH_VALUE()
這幾個函數的滑動窗口範圍是從第一行到當前行(即RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
),直接使用LAST_VALUE()
獲得的結果並非咱們直覺上想看到的那樣。所以,須要把LAST_VALUE()
的窗口範圍改爲RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
。關於滑動窗口更加詳細的描述,在後面有講到。
-
NTILE()
NTILE(N)
將一個分區劃分爲N個組(桶),給分區中的每一行分配其桶號,並返回其分區中當前行的桶號。SELECT empno, deptno, NTILE(2) OVER( PARTITION BY deptno ORDER BY empno) AS ntile2 FROM emp; empno deptno ntile2 ------ ------ -------- 7782 10 1 7839 10 1 7934 10 2 7369 20 1 7566 20 1 7788 20 1 7876 20 2 7902 20 2 7499 30 1 7521 30 1 7654 30 1 7698 30 2 7844 30 2 7900 30 2
-
CUME_DIST()
統計一組數據中小於等於(或大於等於,和
OVER()
中指定的排序行爲有關係)當前行的值的百分比。SELECT sal, ROW_NUMBER() OVER( ORDER BY sal) AS rn, CUME_DIST() OVER( ORDER BY sal) AS dist FROM emp; sal rn dist ------- ------ --------------------- 800.00 1 0.07142857142857142 950.00 2 0.14285714285714285 1100.00 3 0.21428571428571427 1250.00 4 0.35714285714285715 1250.00 5 0.35714285714285715 1300.00 6 0.42857142857142855 1500.00 7 0.5 1600.00 8 0.5714285714285714 2450.00 9 0.6428571428571429 2850.00 10 0.7142857142857143 2975.00 11 0.7857142857142857 3000.00 12 0.9285714285714286 3000.00 13 0.9285714285714286 5000.00 14 1
當咱們在
OVER()
指定排序的行爲是ORDER BY sal DESC
時,看到的倒是另外一番景象。SELECT sal, ROW_NUMBER() OVER( ORDER BY sal DESC) AS rn, CUME_DIST() OVER( ORDER BY sal DESC) AS dist FROM emp; sal rn dist ------- ------ --------------------- 5000.00 1 0.07142857142857142 3000.00 2 0.21428571428571427 3000.00 3 0.21428571428571427 2975.00 4 0.2857142857142857 2850.00 5 0.35714285714285715 2450.00 6 0.42857142857142855 1600.00 7 0.5 1500.00 8 0.5714285714285714 1300.00 9 0.6428571428571429 1250.00 10 0.7857142857142857 1250.00 11 0.7857142857142857 1100.00 12 0.8571428571428571 950.00 13 0.9285714285714286 800.00 14 1
PERCENT_RANK()
和CUME_DIST()
同樣,也是統計某個值的分配狀況,只是它們的算法不同。PERCENT_RANK()
的計算公式:(rank - 1) / (rows - 1)
其中,rank 表示行的等級(若是出現重複值,則用最小的那個編號),rows 表示分區的行數。具體請看下面這個例子。
SELECT empno, sal, ROW_NUMBER() OVER(ORDER BY sal) AS rn, PERCENT_RANK() OVER(ORDER BY sal) AS percent FROM emp; sal rn percent ------- ------ --------------------- 800.00 1 0 950.00 2 0.07692307692307693 1100.00 3 0.15384615384615385 1250.00 4 0.23076923076923078 1250.00 5 0.23076923076923078 1300.00 6 0.38461538461538464 1500.00 7 0.46153846153846156 1600.00 8 0.5384615384615384 2450.00 9 0.6153846153846154 2850.00 10 0.6923076923076923 2975.00 11 0.7692307692307693 3000.00 12 0.8461538461538461 3000.00 13 0.8461538461538461 5000.00 14 1
當 sal = 800 時,percent = (1 -1 ) / (14 - 1) = 0;
當 sal = 1250 時,存在兩個編號:4 跟 5。取最小的編號,則 percent = (4 -1 ) / (14 - 1) = 0.230769;
當 sal = 5000 時, percent = (14 -1 ) / (14 - 1) = 1。
注意,若是在
OVER()
中沒有指定ORDER
子句,那麼PERCENT_RANK()
計算的結果都是同樣的。
2 聚合窗口函數
在下面這些聚合窗口函數後面加上 OVER()
子句,它們就變成了聚合窗口函數。
AVG() BIT_AND() BIT_OR() BIT_XOR() COUNT() JSON_ARRAYAGG() JSON_OBJECTAGG() MAX() MIN() STDDEV_POP(), STDDEV(), STD() STDDEV_SAMP() SUM() VAR_POP(), VARIANCE() VAR_SAMP()
經常使用的聚合窗口函數有:AVG() OVER()
、COUNT() OVER()
、MAX() OVER()
、MIN() OVER()
、SUM() OVER()
。
下面這個例子,它利用窗口函數只查一次 emp 表完成了這些需求:
- 統計全部員工的薪資;
- 統計每一個部門的人數;
- 計算每一個部門的平均薪資;
- 獲取公司裏面的最高薪資;
- 獲取最先入職的員工的入職時間。
SELECT empno AS '編號', ename AS '姓名', sal AS '薪資', deptno AS '部門編號', SUM(sal) OVER() AS '薪資總額', COUNT(*) OVER(PARTITION BY deptno) AS '部門人數', AVG(sal) OVER(PARTITION BY deptno) AS '部門平均薪資', MAX(sal) OVER() AS '最高薪資', MIN(hiredate) OVER() '最先入職時間' FROM emp;
輸出結果>>>
3 命名窗口函數
咱們能夠用 WINDOWS
關鍵字給窗口起別名,並在OVER()
中引用它。命名窗口子句位於 HAVING
子句和 ORDER
子句的位置之間,其語法以下:
WINDOW window_name AS (window_spec) [, window_name AS (window_spec)]
咱們能夠同時定義多個窗口名字。
再來看下 window_spec 的定義:
window_spec: [window_name] [partition_clause] [order_clause] [frame_clause]
也就是說,咱們在定義一個窗口函數時,能夠指定下面這些內容:
- 引用的窗口別名,好比先定義了窗口 w1,再在窗口 w2 中引用 w1,就像這樣
WINDOW w1 AS (w2), w2 AS ()
。可是,不能循環引用。 PARTITION
子句;ORDER
子句;- 滑動窗口範圍。
對於在 SQL 中使用了多個窗口函數,且這些窗口函數中的 OVER()
的內容都相同,使用命名窗口函數就很合適。
好比,對於下面這條 SQL。
SELECT sal, ROW_NUMBER() OVER (ORDER BY sal) AS 'row_number', RANK() OVER (ORDER BY sal) AS 'rank', DENSE_RANK() OVER (ORDER BY sal) AS 'dense_rank' FROM emp;
能夠改形成命名窗口的形式。一旦想改變 OVER()
裏面的內容,只需改動命名窗口裏面的內容,而不用像原來的 SQL 那樣要改動每一個窗口函數的內容。
SELECT sal, ROW_NUMBER() OVER w AS 'row_number', RANK() OVER w AS 'rank', DENSE_RANK() OVER w AS 'dense_rank' FROM emp WINDOW w AS (ORDER BY sal);
咱們再來看一個複雜一點的例子。
SELECT deptno, sal, ROW_NUMBER() OVER w2 AS 'row_number', RANK() OVER (w1 ORDER BY sal) AS 'rank', DENSE_RANK() OVER w3 AS 'dense_rank' FROM emp WINDOW w1 AS(PARTITION BY deptno), w2 AS(ORDER BY sal), w3 AS(w2) ORDER BY sal;
咱們在 RANK() OVER()
的子句裏面引用了窗口 w1,並在其後面接入了 ORDER
子句;在定義窗口 w3 時,咱們直接引用了窗口 w2,即窗口 w3 所表現的行爲和 w2 一致。
實際上,上面這條 SQL 等價於下面這條 SQL。
SELECT deptno, sal, ROW_NUMBER() OVER ( ORDER BY sal) AS 'row_number', RANK() OVER ( PARTITION BY deptno ORDER BY sal) AS 'rank', DENSE_RANK() OVER (ORDER BY sal) AS 'dense_rank' FROM emp ORDER BY sal;
4 動態窗口
咱們來看下 OVER()
的語法結構:
over_clause: {OVER (window_spec) | OVER window_name} window_spec: [window_name] [partition_clause] [order_clause] [frame_clause]
PARTITION
子句和 ORDER
子句你們都比較熟悉了,接下來給你們介紹 FRAME
子句。
FRAME
子句的就是用來實現動態窗口。窗口函數在每行記錄上執行,有的函數的窗口不會發生變化,這種就屬於靜態窗口;有的函數隨着記錄不一樣,窗口大小也在不斷變化,這種就屬於動態窗口。
看下面這個例子,咱們經過滑動窗口實現隨着時間的變化累加部門的薪資,以及計算當前行和上下行記錄的平均薪資。
SELECT empno, deptno, sal, SUM(sal) OVER(PARTITION BY deptno ORDER BY hiredate ROWS UNBOUNDED PRECEDING) AS total, AVG(sal) OVER(PARTITION BY deptno ORDER BY hiredate ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS average FROM emp; empno deptno sal total average ------ ------ ------- -------- ------------- 7782 10 2450.00 2450.00 3725.000000 7839 10 5000.00 7450.00 2916.666667 7934 10 1300.00 8750.00 3150.000000 7369 20 800.00 800.00 1887.500000 7566 20 2975.00 3775.00 2258.333333 7902 20 3000.00 6775.00 2991.666667 7788 20 3000.00 9775.00 2366.666667 7876 20 1100.00 10875.00 2050.000000 7499 30 1600.00 1600.00 1425.000000 7521 30 1250.00 2850.00 1900.000000 7698 30 2850.00 5700.00 1866.666667 7844 30 1500.00 7200.00 1866.666667 7654 30 1250.00 8450.00 1233.333333 7900 30 950.00 9400.00 1100.000000
當計算 empno = 7782 這行記錄時,total = 2450,average = (2450 + 5000)/ 2 = 3725;
當計算 empno = 7839 這行記錄時,total = 2450 + 5000 = 7450,average = (2450 + 5000 + 1300)/ 3 = 2916.66;
當計算 empno = 7934 這行記錄時,total = 2450 + 5000 + 1300 = 8750,average = (5000 + 1300)/ 2 = 3150;
能夠經過基於行或者基於範圍的方式指定窗口的大小:
- 基於行:選擇當前行的先後幾行。好比範圍是當前行的往前兩行和日後三行,就能夠這麼寫語句
ROWS BETWEEN 2 PRECEDING AND 3 FOLLOWING
。 - 基於範圍:選擇數據範圍。例如獲取值在區間 [c-2,c+3]的數據,語句就是
RANGE BETWEEN 2 PRECEDING AND 3 FOLLOWING
,c 表示當前行的值。典型的應用場景是統計天天的日活、月活,這些用基於行的方式很差表示。
咱們再來看關於指定窗口大小的表達式:
CURRENT ROW 邊界是當前行,通常和其餘範圍關鍵字一塊兒使用 UNBOUNDED PRECEDING 邊界是分區中的第一行 UNBOUNDED FOLLOWING 邊界是分區中的最後一行 expr PRECEDING 邊界是當前行減去expr的值 expr FOLLOWING 邊界是當前行加上expr的值
關於 expr 的有效的表達式能夠是:
10 PRECEDING INTERVAL 5 DAY PRECEDING 5 FOLLOWING INTERVAL '2:30' MINUTE_SECOND FOLLOWING
注意,有些窗口函數即便指定了FRAME
子句,在計算的時候仍然選擇的全分區的數據。這些函數包括:
CUME_DIST() DENSE_RANK() LAG() LEAD() NTILE() PERCENT_RANK() RANK() ROW_NUMBER()
FRAME
子句的默認值取決因而否有 ORDER
子句。
- 當存在於
ORDER BY
時,默認值爲分區起始行到當前行,包含和當前行的值相等的其它行。語句至關因而ROW UNBOUNDED PRECEDING
或者ROW BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
。 - 當不存在
ORDER BY
時,默認是分區的全部行,即ROW BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
。
5 窗口函數的限制
- 窗口函數不能直接用在
UPDATE
和DELETE
語句中,但能夠用在子查詢中; - 不支持嵌套窗口函數;
- 聚合窗口函數中不能使用
DISTINCT
; - 依賴於當前行的值的滑動窗口端點。
本文分享自微信公衆號 - SQL實現(gh_684ee9235a26)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。