1. 程式人生 > 資料庫 >Mysql-視窗函式

Mysql-視窗函式

學習連線:https://blog.csdn.net/weixin_39010770/article/details/87862407

 

 

視窗:記錄集合
視窗函式:在滿足某些條件的記錄集合上執行的特殊函式,對於每條記錄都要在此視窗內執行函式。有的函式隨著記錄的不同,視窗大小都是固定的,稱為 靜態視窗;有的函式則相反,不同的記錄對應著不同的視窗,稱為 滑動視窗

1. 視窗函式和普通聚合函式的區別:

①聚合函式是將多條記錄聚合為一條;視窗函式是每條記錄都會執行,有幾條記錄執行完還是幾條。
②聚合函式也可以用於視窗函式。

2. 視窗函式的基本用法:

函式名 OVER 子句

over關鍵字 用來指定函式執行的視窗範圍,若後面括號中什麼都不寫,則意味著視窗包含滿足WHERE條件的所有行,視窗函式基於所有行進行計算;如果不為空,則支援以下4中語法來設定視窗。

①window_name:給視窗指定一個別名。如果SQL中涉及的視窗較多,採用別名可以看起來更清晰易讀;
PARTITION BY 子句:視窗按照哪些欄位進行分組,視窗函式在不同的分組上分別執行;
ORDER BY子句:按照哪些欄位進行排序,視窗函式將按照排序後的記錄順序進行編號;
FRAME子句FRAME 是當前分割槽的一個子集,子句用來定義子集的規則,通常用來作為滑動視窗使用。

# 先看一個例子
SELECT 
    stu_id,
    score,
    sum(score) OVER (PARTITION BY stu_id) AS score_order
FROM t_score;

+--------+-------+-------------+
| stu_id | score | score_order |
+--------+-------+-------------+
|      1 |    90 |         439 |
|      1 |    95 |         439 |
|      1 |    84 |         439 |
|      1 |    75 |         439 |
|      1 |    95 |         439 |
|      2 |    88 |         420 |
|      2 |    68 |         420 |
|      2 |    98 |         420 |
|      2 |    88 |         420 |
|      2 |    78 |         420 |
|      3 |    90 |         427 |
|      3 |    68 |         427 |
|      3 |    85 |         427 |
|      3 |    89 |         427 |
|      3 |    95 |         427 |
|      4 |    68 |         420 |
|      4 |    87 |         420 |
|      4 |    93 |         420 |
|      4 |    87 |         420 |
|      4 |    85 |         420 |
|      5 |    92 |         360 |
|      5 |    92 |         360 |
|      5 |    91 |         360 |
|      5 |    85 |         360 |
+--------+-------+-------------+

 

3. 按功能劃分可將MySQL支援的視窗函式分為如下幾類:

①序號函式:ROW_NUMBER()RANK()DENSE_RANK()
  • 用途:顯示分割槽中的當前行號
  • 應用場景:查詢每個學生的分數最高的前3門課程
ROW_NUMBER() OVER (PARTITION BY stu_id ORDER BY score)

 

SELECT *
FROM (
    SELECT stu_id, ROW_NUMBER() OVER (PARTITION BY stu_id ORDER BY score DESC) AS score_order, lesson_id, score
    FROM t_score
) t
WHERE score_order <= 3;

+--------+-------------+-----------+-------+
| stu_id | score_order | lesson_id | score |
+--------+-------------+-----------+-------+
|      1 |           1 | L002      |    95 |
|      1 |           2 | L005      |    95 |
|      1 |           3 | L001      |    90 |
|      2 |           1 | L003      |    98 |
|      2 |           2 | L001      |    88 |
|      2 |           3 | L004      |    88 |
|      3 |           1 | L005      |    95 |
|      3 |           2 | L001      |    90 |
|      3 |           3 | L004      |    89 |
|      4 |           1 | L003      |    93 |
|      4 |           2 | L002      |    87 |
|      4 |           3 | L004      |    87 |
|      5 |           1 | L001      |    92 |
|      5 |           2 | L002      |    92 |
|      5 |           3 | L003      |    91 |
+--------+-------------+-----------+-------+

 

對於 stu_id=1 的同學,有兩門課程的成績均為98,序號隨機排了1和2。但很多情況下二者應該是並列第一,則他的成績為88的這門課的序號可能是第2名,也可能為第3名。
這時候,ROW_NUMBER() 就不能滿足需求,需要 RANK() 和 DENSE_RANK() 出場,它們和 ROW_NUMBER() 非常類似,只是在出現重複值時處理邏輯有所不同。

 

ROW_NUMBER():順序排序——1、2、3
RANK():並列排序,跳過重複序號——1、1、3
DENSE_RANK():並列排序,不跳過重複序號——1、1、2
SELECT *
FROM (
    SELECT 
        ROW_NUMBER() OVER (PARTITION BY stu_id ORDER BY score DESC) AS score_order1, 
        RANK() OVER (PARTITION BY stu_id ORDER BY score DESC) AS score_order2,
        DENSE_RANK() OVER (PARTITION BY stu_id ORDER BY score DESC) AS score_order3,
        stu_id, lesson_id, score
    FROM t_score
) t
WHERE stu_id = 1
    AND score_order1 <= 3
    AND score_order2 <= 3
    AND score_order3 <= 3;

+--------------+--------------+--------------+--------+-----------+-------+
| score_order1 | score_order2 | score_order3 | stu_id | lesson_id | score |
+--------------+--------------+--------------+--------+-----------+-------+
|            1 |            1 |            1 |      1 | L002      |    95 |
|            2 |            1 |            1 |      1 | L005      |    95 |
|            3 |            3 |            2 |      1 | L001      |    90 |
+--------------+--------------+--------------+--------+-----------+-------+

 

②分佈函式:PERCENT_RANK()CUME_DIST()
PERCENT_RANK()
  • 用途:每行按照公式 (rank-1) / (rows-1) 進行計算。其中,rank 為 RANK() 函式產生的序號,rows 為當前視窗的記錄總行數
  • 應用場景:不常用
給視窗指定別名:WINDOW w AS (PARTITION BY stu_id ORDER BY score)
rows = 5

 

SELECT RANK() OVER w AS rk, PERCENT_RANK() OVER w AS prk, stu_id, lesson_id, score
FROM t_score
WHERE stu_id = 1
WINDOW w AS (PARTITION BY stu_id ORDER BY score);

+----+------+--------+-----------+-------+
| rk | prk  | stu_id | lesson_id | score |
+----+------+--------+-----------+-------+
|  1 |    0 |      1 | L004      |    75 |
|  2 | 0.25 |      1 | L003      |    84 |
|  3 |  0.5 |      1 | L001      |    90 |
|  4 | 0.75 |      1 | L002      |    95 |
|  4 | 0.75 |      1 | L005      |    95 |
+----+------+--------+-----------+-------+

 

CUME_DIST()
  • 用途:分組內小於、等於當前rank值的行數 / 分組內總行數
  • 應用場景:查詢小於等於當前成績(score)的比例
cd1:沒有分割槽,則所有資料均為一組,總行數為8
cd2:按照 lesson_id 分成了兩組,行數各為4

 

SELECT stu_id, lesson_id, score, CUME_DIST() OVER (ORDER BY score) AS cd1
    , CUME_DIST() OVER (PARTITION BY lesson_id ORDER BY score) AS cd2
FROM t_score
WHERE lesson_id IN ('L001', 'L002');

+--------+-----------+-------+-----+-----+
| stu_id | lesson_id | score | cd1 | cd2 |
+--------+-----------+-------+-----+-----+
|      4 | L001      |    68 | 0.3 | 0.2 |
|      2 | L001      |    88 | 0.5 | 0.4 |
|      1 | L001      |    90 | 0.7 | 0.8 |
|      3 | L001      |    90 | 0.7 | 0.8 |
|      5 | L001      |    92 | 0.9 |   1 |
|      2 | L002      |    68 | 0.3 | 0.4 |
|      3 | L002      |    68 | 0.3 | 0.4 |
|      4 | L002      |    87 | 0.4 | 0.6 |
|      5 | L002      |    92 | 0.9 | 0.8 |
|      1 | L002      |    95 |   1 |   1 |
+--------+-----------+-------+-----+-----+

 

③前後函式:LAG(expr,n)LEAD(expr,n)
  • 用途:返回位於當前行的前n行(LAG(expr,n))或後n行(LEAD(expr,n))的expr的值
  • 應用場景:查詢前1名同學的成績和當前同學成績的差值
內層SQL先通過 LAG()函式 得到前1名同學的成績,外層SQL再將當前同學和前1名同學的成績做差得到成績差值 diff。

 

SELECT stu_id, lesson_id, score, pre_score
    , score - pre_score AS diff
FROM (
    SELECT stu_id, lesson_id, score
        , LAG(score, 1) OVER w AS pre_score
    FROM t_score
    WHERE lesson_id IN ('L001', 'L002')
    WINDOW w AS (PARTITION BY lesson_id ORDER BY score)
) t;

+--------+-----------+-------+-----------+------+
| stu_id | lesson_id | score | pre_score | diff |
+--------+-----------+-------+-----------+------+
|      4 | L001      |    68 |      NULL | NULL |
|      2 | L001      |    88 |        68 |   20 |
|      1 | L001      |    90 |        88 |    2 |
|      3 | L001      |    90 |        90 |    0 |
|      5 | L001      |    92 |        90 |    2 |
|      2 | L002      |    68 |      NULL | NULL |
|      3 | L002      |    68 |        68 |    0 |
|      4 | L002      |    87 |        68 |   19 |
|      5 | L002      |    92 |        87 |    5 |
|      1 | L002      |    95 |        92 |    3 |
+--------+-----------+-------+-----------+------+

 

④頭尾函式:FIRST_VALUE(expr)LAST_VALUE(expr)
  • 用途:返回第一個(FIRST_VALUE(expr))或最後一個(LAST_VALUE(expr))expr的值
  • 應用場景:截止到當前成績,按照日期排序查詢第1個和最後1個同學的分數
新增新列:
mysql> ALTER TABLE t_score ADD create_time DATE;

 

SELECT stu_id, lesson_id, score, create_time
    , FIRST_VALUE(score) OVER w AS first_score, LAST_VALUE(score) OVER w AS last_score
FROM t_score
WHERE lesson_id IN ('L001', 'L002')
WINDOW w AS (PARTITION BY lesson_id ORDER BY create_time);

+--------+-----------+-------+-------------+-------------+------------+
| stu_id | lesson_id | score | create_time | first_score | last_score |
+--------+-----------+-------+-------------+-------------+------------+
|      3 | L001      |   100 | 2018-08-07  |         100 |        100 |
|      1 | L001      |    98 | 2018-08-08  |         100 |         98 |
|      2 | L001      |    84 | 2018-08-09  |         100 |         99 |
|      4 | L001      |    99 | 2018-08-09  |         100 |         99 |
|      3 | L002      |    91 | 2018-08-07  |          91 |         91 |
|      1 | L002      |    86 | 2018-08-08  |          91 |         86 |
|      2 | L002      |    90 | 2018-08-09  |          91 |         90 |
|      4 | L002      |    88 | 2018-08-10  |          91 |         88 |
+--------+-----------+-------+-------------+-------------+------------+

 

⑤其它函式:NTH_VALUE(expr, n)NTILE(n)
NTH_VALUE(expr,n)
  • 用途:返回視窗中第n個 expr 的值。expr 可以是表示式,也可以是列名
  • 應用場景:截止到當前成績,顯示每個同學的成績中排名第2和第3的成績的分數
SELECT stu_id, lesson_id, score
    , NTH_VALUE(score, 2) OVER w AS second_score
    , NTH_VALUE(score, 3) OVER w AS third_score
FROM t_score
WHERE stu_id IN (1, 2)
WINDOW w AS (PARTITION BY stu_id ORDER BY score);

+--------+-----------+-------+--------------+-------------+
| stu_id | lesson_id | score | second_score | third_score |
+--------+-----------+-------+--------------+-------------+
|      1 | L004      |    75 |         NULL |        NULL |
|      1 | L003      |    84 |           84 |        NULL |
|      1 | L001      |    90 |           84 |          90 |
|      1 | L002      |    95 |           84 |          90 |
|      1 | L005      |    95 |           84 |          90 |
|      2 | L002      |    68 |         NULL |        NULL |
|      2 | L005      |    78 |           78 |        NULL |
|      2 | L001      |    88 |           78 |          88 |
|      2 | L004      |    88 |           78 |          88 |
|      2 | L003      |    98 |           78 |          88 |
+--------+-----------+-------+--------------+-------------+

 

NTILE(n)
  • 用途:將分割槽中的有序資料分為n個等級,記錄等級數
  • 應用場景:將每門課程按照成績分成3組
SELECT NTILE(3) OVER w AS nf, stu_id, lesson_id, score
FROM t_score
WHERE lesson_id IN ('L001', 'L002')
WINDOW w AS (PARTITION BY lesson_id ORDER BY score);

+------+--------+-----------+-------+
| nf   | stu_id | lesson_id | score |
+------+--------+-----------+-------+
|    1 |      4 | L001      |    68 |
|    1 |      2 | L001      |    88 |
|    2 |      1 | L001      |    90 |
|    2 |      3 | L001      |    90 |
|    3 |      5 | L001      |    92 |
|    1 |      2 | L002      |    68 |
|    1 |      3 | L002      |    68 |
|    2 |      4 | L002      |    87 |
|    2 |      5 | L002      |    92 |
|    3 |      1 | L002      |    95 |
+------+--------+-----------+-------+

 

NTILE(n) 函式在資料分析中應用較多,比如由於資料量大,需要將資料平均分配到n個並行的程序分別計算,此時就可以用NTILE(n)對資料進行分組(由於記錄數不一定被n整除,所以資料不一定完全平均),然後將不同桶號的資料再分配。

4. 聚合函式作為視窗函式:

  • 用途:在視窗中每條記錄動態地應用聚合函式(SUM()AVG()MAX()MIN()COUNT()),可以動態計算在指定的視窗內的各種聚合函式值
  • 應用場景:截止到當前時間,查詢 stu_id=1 的學生的累計分數、分數最高的科目、分數最低的科目
SELECT stu_id, lesson_id, score, create_time
    , FIRST_VALUE(score) OVER w AS first_score
  , LAST_VALUE(score) OVER w AS last_score FROM t_score WHERE lesson_id IN ('L001', 'L002') WINDOW w AS (PARTITION BY lesson_id ORDER BY create_time); +--------+-----------+-------+-------------+-----------+-----------+-----------+ | stu_id | lesson_id | score | create_time | score_sum | score_max | score_min | +--------+-----------+-------+-------------+-----------+-----------+-----------+ | 1 | L001 | 98 | 2018-08-08 | 184 | 98 | 86 | | 1 | L002 | 86 | 2018-08-08 | 184 | 98 | 86 | | 1 | L003 | 79 | 2018-08-09 | 263 | 98 | 79 | | 1 | L004 | 88 | 2018-08-10 | 449 | 98 | 79 | | 1 | L005 | 98 | 2018-08-10 | 449 | 98 | 79 | +--------+-----------+-------+-------------+-----------+-----------+-----------+