窗函數 (SQL)

在SQL中，窗函數(window function)或分析函數(analytic function)是一個函數，它使用來自一行或多行的值來為每一行返回一個值。 與之形成對比，聚合函數為多行返回單個值。窗口函數有一個OVER子句；任何沒有OVER子句的函數都不是窗口函數，而是聚合函數或單行（標量）函數。

**例子1**
例如，這裡有一個查詢，它使用一個窗口函數來比較每個員工的工資與其部門的平均工資（來自PostgreSQL文檔的示例）:
SELECT depname, empno, salary, avg(salary) OVER (PARTITION BY depname) FROM empsalary;
輸出:
depname  
avg          
----------+-------+--------+----------------------
develop   
5020.0000000000000000
develop   
5020.0000000000000000
develop   
5020.0000000000000000
develop   
5020.0000000000000000
develop   
5020.0000000000000000
personnel |     5 |   3500 | 3700.0000000000000000
personnel |     2 |   3900 | 3700.0000000000000000
sales     
4866.6666666666666667
sales     
4866.6666666666666667
sales     
4866.6666666666666667
(10 rows)
PARTITION BY子句將行分組，並且該函數分別應用於每個分組。 如果PARTITION BY子句被省略（例如如果我們有一個空的OVER()子句），那麼整個結果集被視為單個分組。對於此查詢，報告的平均工資將是所有行的平均值。
窗口函數在聚合之後進行評估（例如，在 GROUP BY子句和非窗口聚合函數之後）。   

**語法**
根據PostgreSQL文檔，窗函數具有下列之一的語法:
function_name (expression , expression ... ) OVER window_name
function_name (expression , expression ... ) OVER ( window_definition )
function_name ( * ) OVER window_name
function_name ( * ) OVER ( window_definition )
其中function_name包括：
*開窗函數
**序號函數
***ROW_NUMBER：順序排序——1、2、3
***RANK：並列排序，跳過重複序號——1、1、3
***DENSE_RANK：並列排序，不跳過重複序號——1、1、2
**分佈函數
***PERCENT_RANK
***CUME_DIST
**前驅後繼函數
***LAG(expr,n)：返回當前行的前n行的expr的值
***LEAD(expr,n)：返回當前行的後n行的expr的值
**頭尾函數
***FIRST_VALUE(expr)
***LAST_VALUE(expr)
**其他函數
***NTH_VALUE(expr,n)：返回第n行的expr值
***NTILE(n)：將有序數據分為n個桶，記錄等級數
*所有聚合函數
其中 window_definition具有語法:
existing_window_name 
PARTITION BY expression , ... 
ORDER BY expression  USING operator   LAST }  , ... 
frame_clause 
frame_clause具有下列之一的語法:
{ RANGE | ROWS | GROUPS } frame_start  frame_exclusion 
{ RANGE | ROWS | GROUPS } BETWEEN frame_start AND frame_end  frame_exclusion 
frame_start與frame_end可以是UNBOUNDED PRECEDING, offset PRECEDING, CURRENT ROW, offset FOLLOWING,  UNBOUNDED FOLLOWING. frame_exclusion可以是EXCLUDE CURRENT ROW, EXCLUDE GROUP, EXCLUDE TIES, EXCLUDE NO OTHERS.
expression指不包含窗函數調用的任何表達式。
注:- 方括號指可選子句- 圓括號{}指多種選項的集合，選項之間以豎槓|分割

**例子2**
窗函數允許訪問恰在當前記錄之前和之後的記錄的數據。 一個窗函數定義當前行周圍具有給定行數的幀或窗，並跨窗對數據集執行計算。
NAME |
------------
Aaron| 
SELECT
LAG(name, 1) 
OVER(ORDER BY name) "prev",
name, 
LEAD(name, 1) 
OVER(ORDER BY name) "next"
FROM people
ORDER BY name
查詢結果為:













**例子3**
# 首先創建虛擬的業務員銷售數據 
CREATE TABLE Sales
( 
idate date, 
iname char(2), 
sales int
); 
# 向表中插入數據 
INSERT INTO Sales VALUES 
('2021/1/1', '丁一', 200), 
('2021/2/1', '丁一', 180), 
('2021/2/1', '李四', 100), 
('2021/3/1', '李四', 150), 
('2021/2/1', '劉猛', 180), 
('2021/3/1', '劉猛', 150), 
('2021/1/1', '王二', 200), 
('2021/2/1', '王二', 180), 
('2021/3/1', '王二', 300), 
('2021/1/1', '張三', 300), 
('2021/2/1', '張三', 280), 
('2021/3/1', '張三', 280); 
# 查詢各月中銷售業績最差的業務員
SELECT * FROM 
(SELECT month(idate),iname,sales, 
ROW_NUMBER() OVER(PARTITION BY month(idate) ORDER BY sales) as sales_order 
FROM Sales) as t
WHERE sales_order=1;

**例子4**
求用戶連續登錄天數

# 首先創建虛擬的用戶登錄表，並插入數據 
create table user_login
( 
user_id varchar(100), 
login_time datetime
); 
insert into user_login values 
(1,'2020-11-25 13:21:12'), 
(1,'2020-11-24 13:15:22'), 
(1,'2020-11-24 10:30:15'), 
(1,'2020-11-24 09:18:27'), 
(1,'2020-11-23 07:43:54'), 
(1,'2020-11-10 09:48:36'), 
(1,'2020-11-09 03:30:22'), 
(1,'2020-11-01 15:28:29'), 
(1,'2020-10-31 09:37:45'), 
(2,'2020-11-25 13:54:40'), 
(2,'2020-11-24 13:22:32'), 
(2,'2020-11-23 10:55:52'), 
(2,'2020-11-22 06:30:09'), 
(2,'2020-11-21 08:33:15'), 
(2,'2020-11-20 05:38:18'), 
(2,'2020-11-19 09:21:42'), 
(2,'2020-11-02 00:19:38'), 
(2,'2020-11-01 09:03:11'), 
(2,'2020-10-31 07:44:55'), 
(2,'2020-10-30 08:56:33'), 
(2,'2020-10-29 09:30:28');  

# 第一種情況：查看每位用戶連續登陸的情況。包括每位用戶連續登錄的情況、查看每位用戶最大連續登錄的天數、查看在某個時間段裡連續登錄天數超過N天的用戶
select user_id, 
min(login_date) start_date, 
max(login_date) end_date, 
count(login_date) days # 計算每位用戶連續登錄天數 
from (select *,date_sub(login_date, interval irank day) idate # 增加輔助列，以判斷用戶是否連續登錄 
from (select *,rank() over(partition by user_id order by login_date) irank # 對每個用戶的登錄日期排序 
from (select distinct user_id, date(login_time) login_date from user_login 
# 數據預處理：由於統計的窗口期是天數，對登錄時間字段格式轉換為日期然後去重（去掉用戶一天內多次登錄的情況）
) as a
) as b
) as c 
group by user_id,idate;
# 第二種情況：計算每個用戶最大連續登錄天數 

# 第三種情況：查看給定時間段內連續登錄天數≥5天的用戶  
# 找出相差天數為5的記錄 
select distinct user_id 
from (select *,datediff(idate5,login_date)+1 as days # 計算當前登錄日期與之後第4次登陸的日期的差值 
from (select *,lead(login_date,4) over(partition by user_id order by login_date) idate5 # 獲取當前登錄日期之後第4次登陸的日期
from user_logrin_date) 
as a)
as b 
where days = 5;

**歷史**
引入了窗函數，其後的標準擴展了其功能。
MySQL從8.0開始引入了窗函數。目前支持的語法為：
# 開窗函數語法 
func_name() OVER(PARTITION BY  DESC)

**參考文獻==