MySQL窗口函数的妙用

  • 问题引入

有一张成绩表,里面有若干个学生,他们来自三个班级,每个学生学习了两门课程,现在要求查询出各个班级每门课程的前两名学生id。


  • 分析

要各个班级每门课程的前两名,第一反应肯定要根据班级和课程去分组,但实际上你要是用group by会发现不好处理,因为我们要取每个班的前两名,如果你用group by再用limit,那返回的数据是在总数中的取 n 条记录,而不是每个班取 n 条记录。所以我们得用其他方式实现,比如窗口函数。


  • 窗口函数

窗口函数是可以对数据库进行实时分析处理的函数,可以理解为它是对wheregroup by处理后的结果再进行操作,基本语法如下:

<窗口函数> over (partition by <用于分组的列名> order by <用于排序的列名>)

窗口函数又分为两类:

  1. 专用窗口函数,比如 rank、dense_rank、row_number
  2. 聚合函数,比如sum、avg、count、max、min


  • 用法

回到刚才那个问题,用窗口函数怎么处理呢?

  1. 首先准备一张表,再插入一些数据
create table grade
(
    id        int(10) primary key auto_increment comment '主键',
    stu_id    int(10) comment '学生id',
    class_id  int(10) comment '班级id',
    course_id int(3)  comment '课程id',
    score     int(3)  comment '学生分数',
    unique key (stu_id, course_id)
) engine = innodb
  charset = utf8;
insert into grade (stu_id, class_id, course_id, score) values ('1', 1, 1, 90), ('4', 1, 1, 90),
       ('7', 1, 1, 84), ('10', 1, 1, 84), ('13', 1, 1, 88), ('1', 1, 2, 67), ('4', 1, 2, 85),
       ('7', 1, 2, 90), ('10', 1, 2, 88), ('13', 1, 2, 86);

insert into grade (stu_id, class_id, course_id, score) values ('2', 2, 1, 83), ('5', 2, 1, 94), 
       ('8', 2, 1, 81), ('11', 2, 1, 91), ('14', 2, 1, 79), ('2', 2, 2, 99), ('5', 2, 2, 80),
       ('8', 2, 2, 82), ('11', 2, 2, 76), ('14', 2, 2, 66);

insert into grade (stu_id, class_id, course_id, score) values ('3', 3, 1, 98), ('6', 3, 1, 92),
       ('9', 3, 1, 76), ('12', 3, 1, 73), ('15', 3, 1, 83), ('3', 3, 2, 95), ('6', 3, 2, 91),
       ('9', 3, 2, 86), ('12', 3, 2, 87), ('15', 3, 2, 68);

这里就是建立了一个成绩表,然后往表中插入了15个学生,他们来自三个班级,每个学生学习了两门课程。

  1. rank函数的用法

按照上面窗口函数的语法,写出如下SQL:

select *
from (select *, rank() over (partition by class_id, course_id order by score desc ) as ranking
      from grade) t
where t.ranking <= 2;

先看里面窗口函数那一层,首先是用了rank()partition by class_id, course_id就表示根据class_idcourse_id来分组,order by score desc就表示按照分数降序,然后把分组且分数降序的结果作为ranking字段,就是排名。外层ranking <= 2就表示每个分组取两条数据。

这样查询出来的就满足要求了,为了让结果更加清晰,外层稍微改写一下,不要select *,改成如下所示:

select stu_id,
       case when class_id = 1 then '六(1)班' when class_id = 2 then '六(2)班' else '六(3)班' end as class,
       IF(course_id = 1, '语文', '数学')                                                       as course,
       score,
       ranking
from (select *, rank() over (partition by class_id, course_id order by score desc ) as ranking
      from grade) t
where t.ranking <= 2;

执行结果如下:

结果

可以看到,六(1)班语文有两个90分的,他们并列第一,但是用rank的时候,第二个90分的也占了一个名额,并不会把分数第二大的学生查询出来。

  1. dense_rank()的用法
    直接将rank换成dense_rank就可以看出区别了。
    结果

    dense_rank两个人并列第一名只会占用一个名额,会把分数第二大的也查出来,所以总共查出了13个学生。
  2. row_number()的用法
    换成row_number再看执行结果:


    结果
  • 我用的MySQL5.x,没有这些窗口函数怎么办?

上面说的窗口函数要MySQL8.0+才支持,5.x的话可以自己去实现。比如要实现一个row_number(),格式如下:

select <要查询的字段>,
      ranking
from (select @ranking := if(@<分组字段1> = <分组字段1> and @<分组字段2> = <分组字段2>, @ranking + >1, 1) as ranking,
            @<分组字段1> := <分组字段1>                                                              as <分组字段1>,
            @<分组字段2> := <分组字段2>                                                              as <分组字段2>,
            <排序字段>
            <其他需要查询出来的字段>
     from (select * from <表名> order by <分组字段1>, <分组字段2>, <排序字段> desc) a,
          (select @ranking = 0, @<分组字段一> = 0) b
    ) c
where ranking <= <要取的条数>;

根据题目要求,将分组字段和排序字段代入上面的公式,可得:

select stu_id,
       case when class_id = 1 then '六(1)班' when class_id = 2 then '六(2)班' else '六(3)班' end as class,
       IF(course_id = 1, '语文', '数学')                                                       as course,
       score,
       ranking
from (select @ranking := if(@class_id = class_id and @course_id = course_id, @ranking + 1, 1) as ranking,
             @class_id := class_id                                                            as class_id,
             @course_id := course_id                                                          as course_id,
             score,
             stu_id
      from (select * from grade order by class_id, course_id, score desc) a,
           (select @ranking = 0, @class_id = 0) b
     ) c
where ranking <= 2;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容

  • (2022.04.11 Mon) 窗口函数 窗口函数是作用于数据关系中的一系列记录的SQL函数,这些记录称为窗口(...
    Mc杰夫阅读 463评论 0 0
  • MySQL8.0窗口函数概述 MYSQL 8.0 之后,加入了窗口函数功能,简化了数据分析工作中查询语句的书写 在...
    岁月安暖_1e39阅读 427评论 0 0
  • 一、索引1.聚集索引聚集索引可以理解为顺序排列,比如主键自增的表即为聚集索引,所以聚集索引一个数据库表只能有一个。...
    Ly3911阅读 383评论 0 0
  • RETURNING返回更新后的数据 INSERT INTO ……RETURNING */id UPSERT:INS...
    熊猫学猿阅读 1,447评论 2 5
  • 参考: MySQL 8.0窗口函数:用非常规思维简易实现SQL需求 数分面试-SQL篇 一、mysql窗口函数简介...
    kaka22阅读 1,302评论 0 1