一文搞懂 MySQL 的窗口函數

窗口函數在統計類的需求中很常見,稍微複雜一點的查詢需求就有可能用到它,使用窗口函數能夠極大的簡化咱們的 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 窗口函數的限制

  • 窗口函數不能直接用在 UPDATEDELETE 語句中,但能夠用在子查詢中;
  • 不支持嵌套窗口函數;
  • 聚合窗口函數中不能使用 DISTINCT
  • 依賴於當前行的值的滑動窗口端點。

本文分享自微信公衆號 - SQL實現(gh_684ee9235a26)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索