MySQL 8.0視窗函式:用非常規思維簡易實現SQL需求
一、視窗函式的使用場景
作為IT人士,日常工作中經常會遇到類似這樣的需求:
醫院看病,怎樣知道上次就醫距現在的時間?環比如何計算?怎麼樣得到各部門工資排名前N名員工列表?查詢各部門每人工資佔部門總工資的百分比?
對於這樣的需求,使用傳統的SQL實現起來比較困難。這類需求都有一個共同的特點,需要在單表中滿足某些條件的記錄集內部做一些函式操作,不是簡單的表連線,也不是簡單的聚合可以實現的,通常會讓寫SQL的同學焦頭爛額、絞盡腦汁,費了大半天時間寫出來一堆長長的晦澀難懂的自連線SQL,且效能低下,難以維護。
要解決此類問題,最方便的就是使用視窗函式。
二、MySQL視窗函式簡介
MySQL從8.0開始支援視窗函式,這個功能在大多商業資料庫和部分開源資料庫中早已支援,有的也叫分析函式。
什麼叫視窗?
視窗的概念非常重要,它可以理解為記錄集合,視窗函式也就是在滿足某種條件的記錄集合上執行的特殊函式。 對於每條記錄都要在此視窗內執行函式,有的函式隨著記錄不同,視窗大小都是固定的,這種屬於靜態視窗;有的函式則相反,不同的記錄對應著不同的視窗,這種動態變化的視窗叫滑動視窗。
視窗函式和普通聚合函式也很容易混淆,二者區別如下:
-
聚合函式是將多條記錄聚合為一條;而視窗函式是每條記錄都會執行,有幾條記錄執行完還是幾條。
-
聚合函式也可以用於視窗函式中,這個後面會舉例說明。
下面是一個視窗函式的簡單例 子 :
上面例子中,row_number()over(partition by user_no order by amount desc)這部分都屬於視窗函式,它的功能是顯示每個使用者按照訂單金額從大到小排序的序號。
按照功能劃分,可以把MySQL支援的視窗函式分為如下幾類:
-
序號函式:row_number() / rank() / dense_rank()
-
分佈函式:percent_rank() / cume_dist()
-
前後函式:lag() / lead()
-
頭尾函式:first_val() / last_val()
-
其他函式:nth_value() / nfile()
三、視窗函式如何使用
視窗函式的基本用法如下:
函式名([expr]) over子句
其中,over是關鍵字,用來指定函式執行的視窗範圍,如果後面括號中什麼都不寫,則意味著視窗包含滿足where條件的所有行,視窗函式基於所有行進行計算;如果不為空,則支援以下四種語法來設定視窗:
-
window_name: 給視窗指定一個別名,如果SQL中涉及的視窗較多,採用別名可以看起來更清晰易讀。上面例子中如果指定一個別名w,則改寫如下:
select * from ( select row_number()over w as row_num, order_id,user_no,amount,create_date from order_tab WINDOW w AS (partition by user_no order by amount desc) )t ;
-
partition子句: 視窗按照那些欄位進行分組,視窗函式在不同的分組上分別執行。上面的例子就按照使用者id進行了分組。在每個使用者id上,按照order by的順序分別生成從1開始的順序編號。
-
order by子句: 按照哪些欄位進行排序,視窗函式將按照排序後的記錄順序進行編號。可以和partition子句配合使用,也可以單獨使用。上例中二者同時使用,如果沒有partition子句,則會按照所有使用者的訂單金額排序來生成序號。
-
frame子句: frame是當前分割槽的一個子集,子句用來定義子集的規則,通常用來作為滑動視窗使用。比如要根據每個訂單動態計算包括本訂單和按時間順序前後兩個訂單的平均訂單金額,則可以設定如下frame子句來建立滑動窗 口 :
從結果可以看出,order_id為5訂單屬於邊界值,沒有前一行,因此平均訂單金額為(900+800)/2=850;order_id為4的訂單前後都有訂單,所以平均訂單金額為(900+800+300)/3=666.6667,以此類推就可以得到一個基於滑動視窗的動態平均訂單值。此例中,視窗函式用到了傳統的聚合函式avg(),用來計算動態的平均值。
對於滑動視窗的範圍指定,有兩種方式,基於行和基於範圍,具體區別如下:
基於行:
通常使用BETWEEN frame_start AND frame_end語法來表示行範圍,frame_start和frame_end可以支援如下關鍵字,來確定不同的動態行記錄:
CURRENT ROW 邊界是當前行,一般和其他範圍關鍵字一起使用 UNBOUNDED PRECEDING 邊界是分割槽中的第一行 UNBOUNDED FOLLOWING 邊界是分割槽中的最後一行 expr PRECEDING 邊界是當前行減去expr的值 expr FOLLOWING 邊界是當前行加上expr的值
比如,下面都是合法的範圍:
rows BETWEEN 1 PRECEDING AND 1 FOLLOWING 視窗範圍是當前行、前一行、後一行一共三行記錄。 rows UNBOUNDED FOLLOWING 視窗範圍是當前行到分割槽中的最後一行。 rows BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 視窗範圍是當前分割槽中所有行,等同於不寫。
基於範圍:
和基於行類似,但有些範圍不是直接可以用行數來表示的,比如希望視窗範圍是一週前的訂單開始,截止到當前行,則無法使用rows來直接表示,此時就可以使用範圍來表示視窗:INTERVAL 7 DAY PRECEDING。Linux中常見的最近1分鐘、5分鐘負載是一個典型的應用場景。
有的函式不管有沒有frame子句,它的視窗都是固定的,也就是前面介紹的靜態視窗,這些函式包括如下:
-
CUME_DIST()
-
DENSE_RANK()
-
LAG()
-
LEAD()
-
NTILE()
-
PERCENT_RANK()
-
RANK()
-
ROW_NUMBER()
接下來我們以上例的訂單表為例,來介紹每個函式的使用方法。表中各欄位含義按順序分別為訂單號、使用者id、訂單金額、訂單建立日期。
四、序號函式
序號函式——row_number() / rank() / dense_rank()。
-
用途:顯示分割槽中的當前行號
-
使用場景:希望查詢每個使用者訂單金額最高的前三個訂單
此時可以使用ROW_NUMBER()函式按照使用者進行分組並按照訂單日期進行由大到小排序,最後查詢每組中序號<=3的記錄。
對於使用者‘002’的訂單,大家發現訂單金額為800的有兩條,序號隨機排了1和2,但很多情況下二者應該是並列第一,而訂單為600的序號則可能是第二名,也可能為第三名,這時候,row_number就不能滿足需求,需要rank和dense_rank出場。
這兩個函式和row_number()非常類似,只是在出現重複值時處理邏輯有所不同。
上面例子我們稍微改一下,需要查詢不同使用者的訂單中,按照訂單金額進行排序,顯示出相應的排名序號,SQL中用row_number() / rank() / dense_rank()分別顯示序號,我們看一下有什麼差別:
上面紅色粗體顯示了三個函式的區別,row_number()在amount都是800的兩條記錄上隨機排序,但序號按照1、2遞增,後面amount為600的的序號繼續遞增為3,中間不會產生序號間隙;rank()/dense_rank()則把amount為800的兩條記錄序號都設定為1,但後續amount為600的需要則分別設定為3(rank)和2(dense_rank)。即rank()會產生序號相同的記錄,同時可能產生序號間隙;而dense_rank()也會產生序號相同的記錄,但不會產生序號間隙。
五、分佈函式
分佈函式——perce nt_rank()/cume_dist()。
percent_rank()
-
用途:和之前的RANK()函式相關,每行按照如下公式進行計算:
(rank - 1) / (rows - 1)
其中,rank為RANK()函式產生的序號,rows為當前視窗的記錄總行數。
-
應用場景:沒想出來……感覺不太常用,看個例子吧↓
從結果看出,percent列按照公式(rank - 1) / (rows - 1)帶入rank值(row_num列)和rows值(user_no為‘001’和‘002’的值均為5)。
cume_dist()
-
用途:分組內小於等於當前rank值的行數/分組內總行數,這個函式比percen_rank使用場景更多。
-
應用場景:大於等於當前訂單金額的訂單比例有多少。
SQL如下:
列cume顯示了預期的資料分佈結果。
六、前後函式
前後函式——l e ad(n)/lag(n)。
-
用途:分割槽中位於當前行前n行(lead)/後n行(lag)的記錄值。
-
使用場景:查詢上一個訂單距離當前訂單的時間間隔。
SQL如下:
內層SQL先通過lag函式得到上一次訂單的日期,外層SQL再將本次訂單和上次訂單日期做差得到時間間隔diff。
七、頭尾函式
頭尾函式——first_ val(expr)/last_val(expr)。
-
用途:得到分割槽中的第一個/最後一個指定引數的值。
-
使用場景:查詢截止到當前訂單,按照日期排序第一個訂單和最後一個訂單的訂單金額。
SQL如下:
結果和預期一致,比如order_id為4的記錄,first_amount和last_amount分別記錄了使用者‘001’截止到時間2018-01-03 00:00:00為止,第一條訂單金額100和最後一條訂單金額800,注意這裡是按時間排序的最早訂單和最晚訂單,並不是最小金額和最大金額訂單。
八、其他函式
其他函式——nth _value(expr,n)/nfile(n)。
nth_value(expr,n)
-
用途:返回視窗中第N個expr的值,expr可以是表示式,也可以是列名。
-
應用場景:每個使用者訂單中顯示本使用者金額排名第二和第三的訂單金額。
SQL如下:
nfile(n)
-
用途:將分割槽中的有序資料分為n個桶,記錄桶號。
-
應用場景:將每個使用者的訂單按照訂單金額分成3組。
SQL如下:
此函式在資料分析中應用較多,比如由於資料量大,需要將資料平均分配到N個並行的程序分別計算,此時就可以用NFILE(N)對資料進行分組,由於記錄數不一定被N整除,所以資料不一定完全平均,然後將不同桶號的資料再分配。
九、聚合函式作為視窗函式
-
用途:在視窗中每條記錄動態應用聚合函式(sum/avg/max/min/count),可以動態計算在指定的視窗內的各種聚合函式值。
-
應用場景:每個使用者按照訂單id,截止到當前的累計訂單金額/平均訂單金額/最大訂單金額/最小訂單金額/訂單數是多少?
SQL如下:
除了這幾個常用的聚合函式,還有一些也可以使用,比如BIT_AND()、STD()等等,具體檢視官方文件。
視窗函式非常有意思,對於一些使用常規思維無法實現的SQL需求,大家嘗試一下視窗函式吧,相信會有意想不到的收穫。