開窗函數


當我們對於一些查詢條件需要用到復雜子查詢時,聚合函數操作起來非常麻煩,因此使用開窗函數能夠輕松實現

窗口函數的引入是為了解決想要既顯示聚集前的數據,又要顯示聚集后的數據。 開窗函數對一組值進行操作,不需要使用GROUP BY子句對數據進行分組,能夠在同一行中同時返回基礎行的列和聚合列。

強調:使用 mysql8.0版本方可實現

語法:函數名(列) over(選項) 選項可以為 partition by 列 order by 列

over() 按所有行進行分組

over(partition by xxx) 按xxx分組的所有行進行分組 over(partition by xxx order by aaa) 按列xxx分組,按列aaa排序

over(order by aaa) 按aaa列排序

over括號中的partition by和order by的使用根據具體情況選擇

示例

數據在本文的最后

開窗函數的分類

  • 聚合開窗函數

函數名如果是聚合函數,則成為聚合開窗函數

語法:聚合函數(列) over(partition by 列 order by 列)

常見的聚合函數有:sum() count() average() max() min()

需求:計算每個學生的及格科目數

聚合函數執行結果

select student_id,count(sid) from  score where num>= 60 group by student_id;

 

 

我們可以看出,通過普通的聚合函數分組計算后,數據表結構發生了變化,他會根據分組進行顯示,並且,如果你是根據學生ID分組,那你查詢的字段應該也是學生ID,不然會影響到分組結果所對應的數值,例如現在查詢條件再添加一個SId

select sid,student_id,count(sid) from  score where num>= 60 group by student_id;

我們會發現sid的數據並沒有實際意義,因為數據表已經根據分組發生了變化。

開窗函數的執行結果

select sid,student_id,count(sid) over(PARTITION by student_id order
 by student_id) 及格數   from score where num>= 60;

 

總結:我們會發現開窗函數不會修改源數據表的結構,也是在表的最后一列添加想要的結果,如果分組存在多行數據,則重復顯示,因此對於既想要分組結果,又不想改變數據表的結構時,使用開窗函數效果非常好,但是對於聚合開窗函數來說,個人覺得大部分情況下還是采用聚合函數比較多。

對於排序來說,開窗函數確實好用

  • 排序開窗函數

row_number(行號)

rank(排名)

dense_rank(密集排名)

ntile(分組排名)

都是排名函數,不同之處在於對於名次相同的數據處理方式

我們通過一個實例,來區分它們之間的不同之處

需求:查詢各科成績前三名的學生可成績信息

如果使用聚合函數就比較麻煩了,再考慮到分數相同的情況的話會更麻煩,要多層嵌套才能實現,因此這個時候就凸顯開窗函數的優勢了

對每門課程進行分組排序,然后取出前三名即可

step1 先對所有數據進行排序

select s.sid,s1.sname,s1.gender,c.cname,s.num,   row_number() over 
(partition by c.cname order by num desc) as row_number排名,   
rank() over (partition by c.cname order by num desc) as rank排名,   
dense_rank() over (partition by c.cname order by num desc) as dense_rank排名,   
ntile(6) over (partition by c.cname order by num desc) as ntile排名    
from score s   join student s1 on s.student_id = s1.sid   
left join course c on s.course_id = c.cid

結果如下

 

我們一個一個來分析

row_number

原理:根據課程進行分組,然后對每組內的成績進行降序排序

我們可以看出row_number對於同組內的相同成績並沒有做特殊處理,而僅僅是生成連續的序號,因此用row_number 做成績排序貌似不准備,當然它通常也不用在此處,這里只是為了方便對比,row_number 常用於按照某列生成連續序號,例如web程序的分頁等等

rank

rank函數用於返回結果集的分區內每行的排名,簡單來說rank函數就是對查詢出來的記錄進行排名,與row_number函數不同的是,rank函數考慮到了over子句中排序字段值相同的情況,over子句中排序字段值相同的序號是一樣的,后面字段值不相同的序號將跳過相同的排名號排下一個,也就是相關行之前的排名數加一,通過上面的例子我們也可以看出,rank考慮到值相同情況,並且它的排名存在跳躍性。

dense_rank

從字面意思理解,密集排名,也是他在考慮了值相同時排名也相同,但是序號不跳躍,緊跟上一個序號,例如題目中體育成績有2位同學(張三和劉三)並列第一,如果使用rank排名 ,那鋼蛋就是第三名,而如果采用dense_rank 那鋼蛋就是第二名,這個很容易理解吧。

ntile

我們從代碼中可以看出,ntile()中有個數字,那其實ntile有一個叫“桶”的概念

原理是這樣的

首先,ntile會先根據你的分組依據,本題中是課程名稱,然后把每個組的總記錄數進行按照你給的ntile()里的數字進行,這個數字就是桶數,相當於是把體育課程總共12條記錄,盡量等划分成5桶,然后按照num的排序等級划分,每個桶兩條記錄,也就是112233445566的排序結果了,很顯然,這個排序結果的數字大小只能用於桶與桶之間,而桶內部記錄雖然序號相同,但是num不一定相同。

​ 回到本題當中

​ 統計各科成績前三,那很顯然,采用dense_rank 更合適

​ 代碼如下

select * from 
(select s.sid,s1.sname,s1.gender,c.cname,s.num,dense_rank() 
over (partition by c.cname order by num desc) as dense_rank排名 from score s
join student s1 on s.student_id = s1.sid
left join course c on s.course_id = c.cid) as e
where dense_rank排名 <= 3;
  • 其他

lag(col,n)

用於統計窗口內往上第n行值

lead(col,n)

用於統計窗口內往下第n行值

這兩個函數可以用於同列中相鄰行的數據相減操作

需求:對於下面的數據,對於同一用戶(uid)如果在2分鍾之內重新登錄,則判斷為作弊,統計哪些用戶有作弊行為,並計算作弊次數

 

數據代碼

create table lag_table(id int primary key,
                       uid int not null,
                       login_time datetime not null);
insert into lag_table values(1,1,"2020-4-10 12:02:00"),
                            (2,1,"2020-4-10 12:03:23"),
                            (3,1,"2020-4-10 12:03:59"),
                            (4,1,"2020-4-10 12:06:34"),
                            (5,2,"2020-4-10 13:00:00"),
                            (6,2,"2020-4-10 13:02:00"),
                            (7,2,"2020-4-10 13:02:45")  

思路:

根據題目要求,如果能把相鄰兩列的下面那一列與上面那一列變成同一行,不就能實現相減了么,因此我們可以多生成一列,例如:我們可以把uid都為1的第二行記錄生成到第一行,以此類推,這就可以用到lead往下移動的操作了。

select id,uid,login_time,lead(login_time,1) over(partition by uid order by login_time) lead_time from lag_table;

 

我們發現,根據不同用戶,第二行的數據已經移動到第一行了,接下來進行相減操作就可以了

select *,format(相差秒數/60,3) 相差分鍾數 from    
(select id,uid,login_time,
lead(login_time,1) over(partition by uid order by login_time) lead_time,  
TIMESTAMPDIFF(SECOND,login_time,(lead(login_time,1) over(partition by uid order by login_time)))  相差秒數   
from lag_table) 
as e

現在進行相減操作

 

這里之所以相減單位設置為秒,是因為使用TIMESTAMPDIFF之后,會進行四舍五入,如果是2.3分鍾的話,原則已經不算作弊了,但是我們計算時會統計上的,所以采用秒進行換算。

最終結果

select uid,count(1) 作弊次數 from    
(select id,uid,login_time,
lead(login_time,1) over(partition by uid order by login_time) lead_time,   
TIMESTAMPDIFF(SECOND,login_time,(lead(login_time,1) over(partition by uid order by login_time)))  相差秒數   
from lag_table) as e   
where format(相差秒數/60,3)<=2   
group by uid;

 

這里也可以考慮使用lag函數,只是相減的對象互換一下

select id,uid,login_time,lag(login_time,1) 
over(partition by uid order by login_time) lead_time,   
TIMESTAMPDIFF(SECOND,(lag(login_time,1) over(partition by uid order by login_time)),login_time)  相差秒數   
from lag_table

結果

計算相差秒數及最終結果

select uid,count(1) 作弊次數 from    
(select id,uid,login_time,
lag(login_time,1) over(partition by uid order by login_time) lead_time,   
TIMESTAMPDIFF(SECOND,(lag(login_time,1) over(partition by uid order by login_time)),login_time)  相差秒數   
from lag_table) as e   where format(相差秒數/60,3)<=2   group by uid;

first_value(column)

取分組內排序后,截止到當前行,第一個值

這個舉個例子就明白了

select s.sid,s1.sname,s1.gender,c.cname,s.num,
first_value(num) over(partition by c.cname order by num desc) as first_value用法 
from score s   join student s1 on s.student_id = s1.sid   
left join course c on s.course_id = c.cid

 

根據分組排序后,每組按照排序后第一個值進行顯示

last_value(column)

取分組內排序后,截止到當前行,最后一個值

select s.sid,s1.sname,s1.gender,c.cname,s.num,
last_value(num) over(partition by c.cname ) as last_value用法 
from score s   join student s1 on s.student_id = s1.sid   
left join course c on s.course_id = c.cid

咦,為啥這里的last_value的用法不是按照每個組的最后一個值,也就是所謂的最小值來取值的呢?好像一個組中顯示的結果也不一樣,看着也沒啥規律呀

其實,事實是這樣的

last_value()默認統計范圍是 rows between unbounded preceding and current row,也就是取當前行數據與當前行之前的數據的比較。

 

那我得改一下呀,這不是我們想要的效果,怎么改呢?

在order by 條件的后面加上語句:rows between unbounded preceding and unbounded following

可以理解為:當前分組數據中的所有數據進行比較,取最后一條記錄

修改SQL

select s.sid,s1.sname,s1.gender,c.cname,s.num, last_value(num) over(partition by c.cname order by num desc rows between unbounded preceding and unbounded following) as last_value用法 from score s join student s1 on s.student_id = s1.sid left join course c on s.course_id = c.cid

達到了我們想要的效果

詳細解釋:

rows beteween XXX and XXX

unbounded 無限制的

preceding 分區的當前記錄的向前偏移量

current 當前

following 分區的當前記錄的向后偏移量

附加思考

面試時有沒有被問到過如何累計計算每個月的銷售額

數據准備

某公司銷售數據表

需求:計算每個月的銷售額及累計銷售額,結果如下:

代碼

select 年份,月份,sum(銷售金額) 每月銷售額,sum(sum(銷售金額)) over(order by 月份
rows between unbounded preceding and current row)  as 累計銷售額 
from sale group by 年份,月份;

數據庫相關知識推薦:

ailsa:13 MySQL模塊: 記錄的增刪改查​zhuanlan.zhihu.com圖標 ailsa:12 MySQL模塊:庫表操作(DDL)​zhuanlan.zhihu.com圖標

注:

示例1--數據

CREATE TABLE class (
  cid int(11) NOT NULL AUTO_INCREMENT,
  caption varchar(32) NOT NULL,
  PRIMARY KEY (cid)
) ENGINE=InnoDB CHARSET=utf8;

INSERT INTO class VALUES
(1, '三年二班'), 
(2, '三年三班'), 
(3, '一年二班'), 
(4, '二年九班');

CREATE TABLE teacher(
  tid int(11) NOT NULL AUTO_INCREMENT,
  tname varchar(32) NOT NULL,
  PRIMARY KEY (tid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO teacher VALUES
(1, '張磊老師'), 
(2, '李平老師'), 
(3, '劉海燕老師'), 
(4, '朱雲海老師'), 
(5, '李傑老師');

CREATE TABLE course(
  cid int(11) NOT NULL AUTO_INCREMENT,
  cname varchar(32) NOT NULL,
  teacher_id int(11) NOT NULL,
  PRIMARY KEY (cid),
  KEY fk_course_teacher (teacher_id),
  CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher (tid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO course VALUES
(1, '生物', 1), 
(2, '物理', 2), 
(3, '體育', 3), 
(4, '美術', 2);

CREATE TABLE student(
  sid int(11) NOT NULL AUTO_INCREMENT,
  gender char(1) NOT NULL,
  class_id int(11) NOT NULL,
  sname varchar(32) NOT NULL,
  PRIMARY KEY (sid),
  KEY fk_class (class_id),
  CONSTRAINT fk_class FOREIGN KEY (class_id) REFERENCES class (cid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO student VALUES
(1, '男', 1, '理解'), 
(2, '女', 1, '鋼蛋'), 
(3, '男', 1, '張三'), 
(4, '男', 1, '張一'), 
(5, '女', 1, '張二'), 
(6, '男', 1, '張四'), 
(7, '女', 2, '鐵錘'), 
(8, '男', 2, '李三'), 
(9, '男', 2, '李一'), 
(10, '女', 2, '李二'), 
(11, '男', 2, '李四'), 
(12, '女', 3, '如花'), 
(13, '男', 3, '劉三'), 
(14, '男', 3, '劉一'), 
(15, '女', 3, '劉二'), 
(16, '男', 3, '劉四');

CREATE TABLE score (
  sid int(11) NOT NULL AUTO_INCREMENT,
  student_id int(11) NOT NULL,
  course_id int(11) NOT NULL,
  num int(11) NOT NULL,
  PRIMARY KEY (sid),
  KEY fk_score_student (student_id),
  KEY fk_score_course (course_id),
  CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course (cid),
  CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(sid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO score VALUES
(1, 1, 1, 10),
(2, 1, 2, 9),
(5, 1, 4, 66),
(6, 2, 1, 8),
(8, 2, 3, 68),
(9, 2, 4, 99),
(10, 3, 1, 77),
(11, 3, 2, 66),
(12, 3, 3, 87),
(13, 3, 4, 99),
(14, 4, 1, 79),
(15, 4, 2, 11),
(16, 4, 3, 67),
(17, 4, 4, 100),
(18, 5, 1, 79),
(19, 5, 2, 11),
(20, 5, 3, 67),
(21, 5, 4, 100),
(22, 6, 1, 9),
(23, 6, 2, 100),
(24, 6, 3, 67),
(25, 6, 4, 100),
(26, 7, 1, 9),
(27, 7, 2, 100),
(28, 7, 3, 67),
(29, 7, 4, 88),
(30, 8, 1, 9),
(31, 8, 2, 100),
(32, 8, 3, 67),
(33, 8, 4, 88),
(34, 9, 1, 91),
(35, 9, 2, 88),
(36, 9, 3, 67),
(37, 9, 4, 22),
(38, 10, 1, 90),
(39, 10, 2, 77),
(40, 10, 3, 43),
(41, 10, 4, 87),
(42, 11, 1, 90),
(43, 11, 2, 77),
(44, 11, 3, 43),
(45, 11, 4, 87),
(46, 12, 1, 90),
(47, 12, 2, 77),
(48, 12, 3, 43),
(49, 12, 4, 87),
(52, 13, 3, 87);

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM