Post

SQL窗口函数实践

当年在校招面试时,面试官出了一道SQL题:一次年级考试,求每个班级内的成绩前10名。

最近又遇到一个查询需求,假设一张表有(id,a,b,c)字段,需要一次批量查出:

  • 查询 a = 1 and b = 2 and c = 3 条件下的max(id);
  • 查询 a = 2 and b = 1 and c = 4 条件下的max(id);
  • 查询 a = 4 and b = 3 and c = 6 条件下的max(id);
  • … 以此类推,需要批量查多条件下的最大id。

使用窗口函数即可以一次满足上面两个查询需求。

先看下窗口函数的定义:

In SQL, a window function or analytic function is a function which uses values from one or multiple rows to return a value for each row. (This contrasts with an aggregate function, which returns a single value for multiple rows.) Window functions have an OVER clause; any function without an OVER clause is not a window function, but rather an aggregate or single-row (scalar) function.

可以看出,窗口函数即是对每一个row都会生成对应的row数据(n:n),而aggregate function可以聚合多行成一行数据(n:1)。

窗口函数表达式如下:

1
function(args) OVER ([PARTITION BY expression] [ORDER By expression [ASC|DESC]] [frame])

其中 partition by 表示根据字段进行分组求窗口函数,不加则表示所有行;order by 表示一个分组内的排序规则;[frame]表示滑动行范围。

接下来根据不同类型的function结合案例讲解使用方式。

排序函数

下面给出了3种不同规则的排序函数

  • row_number() : 1,2,3,…
  • rank() : 1,2,2,4,…
  • dense_rank() : 1,2,2,3,…

假设有一张考试成绩表:

1
2
3
4
5
6
CREATE TABLE `exam_result` (
  `student_name` varchar(15) COMMENT '学生姓名',
  `grade` int COMMENT '分数',
  `class` int COMMENT '班级',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

其中有数据:

student_namegradeclass
张三891
李四931
王五781
小明872
小赵552
小方1002

需要求每个班级下的分数前2名,则SQL如下:

1
2
3
select ranks.rank_in_class, ranks.student_name , ranks.class from
(select  student_name, class, row_number() over (partition by class order by grade desc) as rank_in_class
from exam_result ) ranks where  ranks.rank_in_class <= 2;

则输出如下:

rank_in_classstudent_nameclass
1李四1
2张三1
1小方1
2小明2

符合预期。


假设在班级1中有两个分数为89的学生:

student_namegradeclass
张三891
张四891
李四931
王五781
小明872
小赵552
小方1002

我们要查出每个班级的第二名,且分数一样就都是第二名,SQL如下:

1
2
3
4
select ranks.student_name, ranks.class from
-- 使用rank()和dense_rank()都可以
( select  student_name, class, rank() over (partition by class order by grade desc) as rank_in_class
from exam_result ) ranks where ranks.rank_in_class = 2;

则输出如下:

student_nameclass
张三1
张四1
小明2

那如果存在同分的情况下要查处第三名呢?按照rank()我们发现第二名同分两个,所以第三名没有,而dense_rank()有

聚合函数

聚合函数:

  • COUNT
  • AVG
  • SUM
  • MAX
  • MIN
  • FIRST
  • LAST

滑动行范围模式:

  • rows模式
  • range模式

滑动行范围的常用表达:

1
2
3
4
5
6
7
8
9
10
11
12
{RANGE|ROWS} frame_start
{RANGE|ROWS} BETWEEN frame_start AND frame_end

frame_startframe_end可以取值:
UNBOUNDED PRECEDING
expression PRECEDING --only allowed in ROWS mode
CURRENT ROW
express FOLLOWING --only allowed in ROWS mode
UNBOUNDED FOLLOWING

默认:
BETWEEN unbounded preceding AND CURRENT ROW

同样是上面的exam_result表,表中有数据:

student_namegradeclass
张三891
李四931
王五781
小明872
小赵552
小方1002

现在要求每个班的最高分,SQL如下:

1
2
3
-- 最高分
select distinct  class, max(grade) over (partition by class ) as max_grade
from exam_result;

输出如下:

classmax_grade
193
2100
1
2
3
-- 平均分
select distinct class, avg(grade) over (partition by class) as avg_grade
from exam_result
classavg_grade
186.6667
280.6667
1
2
3
-- 但是这里根据grade order一下,输出了奇怪的数据
select distinct class, avg(grade) over (partition by class order by grade ) as avg_grade
from exam_result

奇怪的数据如下:

classavg_grade
178.0000
183.5000
186.6667
255.0000
271.0000
280.6667

为什么,因为指定了order没有指定滑动行范围,则默认为第一行到当前行,比如这里 78.0000是按照class为1时的grade升序后第一个做平均,83.5000是前两个做平均,86.6667前3个做平均,所以别乱加order。


同样有以下数据:

student_namegradeclass
张三891
李四931
王五781
小明872
小赵552
小方1002

奇怪一点的case,想求当前同学的分数和班级内比自己小的一个分数的这两个分数的平均值(最小的就自己和自己平均)。 比如班级1,分数78最低,和自己的平均值就是78; 分数89,和比自己低的78取平均就是83.5000;分数93,和89取平均,就是91.0000。

则使用滑动行的ROWS模式,SQL如下:

1
2
3
4
5
6
7
8
9
select class, 
       student_name, 
       avg(grade) over (partition by class order by grade ROWS 1 PRECEDING) as avg_grade
from exam_result;
-- 等价于
select class,
       student_name,
       avg(grade) over (partition by class order by grade ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) as avg_grade
from exam_result;

输出如下:

classstudent_nameavg_grade
1王五78.0000
1张三83.5000 (89和78取平均)
1李四91.0000 (93和89取平均)
2小赵55.0000
2小明71.0000 (87和55取平均)
2小方93.5000 (100和87取平均)

同样有以下数据:

student_namegradeclass
张三891
李四931
王五781
小明872
小赵552
小方1002

更奇怪一点的case,求与自己相差+-10分以内的分数平均值。比如班级1的89分,和93分算平均值,而78和89差了超过10,就和89算平均。SQL如下:

1
2
3
4
select class,
       student_name,
       avg(grade) over (partition by class order by grade RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING) as avg_grade
from exam_result;

输出如下:

classstudent_nameavg_grade
1王五78.0000
1张三91.0000(89和93取平均)
1李四91.0000 (89和93取平均)
2小赵55.0000
2小明87.0000
2小方100.0000
This post is licensed under CC BY 4.0 by the author.