前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >HiveSQL分析函数实践详解

HiveSQL分析函数实践详解

作者头像
Spark学习技巧
发布于 2024-04-12 07:00:45
发布于 2024-04-12 07:00:45
54800
代码可运行
举报
文章被收录于专栏:Spark学习技巧Spark学习技巧
运行总次数:0
代码可运行

一、窗口函数概述:

窗口函数也称为OLAP函数,OLAP 是OnLine Analytical Processing 的简称,意思是对数据库数据进行实时分析处理。例如,市场分析、创建财务报表、创建计划等日常性商务工作。窗口函数就是为了实现OLAP 而添加的标准SQL 功能。

1. 窗口函数的分类

按照功能划分:

  • 序号函数:row_number() / rank() / dense_rank()
  • 分布函数:percent_rank() / cume_dist()
  • 前后函数:lag() / lead()
  • 头尾函数:first_val() / last_val()
  • 聚合函数+窗口函数联合:
    • 求和 sum() over()
    • 求最大/小 max()/min() over()
    • 求平均 avg() over()
  • 其他函数:nth_value() / nfile()

如上,窗口函数的用法多种多样,不仅有专门的的窗口函数,还可以与聚合函数配合使用。

2. 窗口函数与普通聚合函数的区别:

聚合函数是将多条记录聚合为一条;窗口函数是每条记录都会执行,有几条记录执行完还是几条。

窗口函数兼具GROUP BY 子句的分组功能以及ORDER BY 子句的排序功能。但是,PARTITION BY 子句并不具备 GROUP BY 子句的汇总功能。

举例:若原表中有id一样的10行数据,使用GROUP BY,返回的结果是将多条记录聚合成一条;而使用 rank() 等窗口函数并不会减少原表中 记录的行数,结果中仍然包含 10 行数据。

窗口函数兼具分组和排序两种功能。

二、窗口函数的基本用法

如有基础数据:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
drop table if exists exam_record;
CREATE TABLE exam_record (
    id int PRIMARY KEY AUTO_INCREMENT COMMENT '自增ID',
    uid int NOT NULL COMMENT '用户ID',
    exam_id int NOT NULL COMMENT '试卷ID',
    start_time datetime NOT NULL COMMENT '开始时间',
    submit_time datetime COMMENT '提交时间',
    score tinyint COMMENT '得分'
)CHARACTER SET utf8 COLLATE utf8_general_ci;
 
INSERT INTO exam_record(uid,exam_id,start_time,submit_time,score) VALUES
(1006, 9003, '2021-09-07 10:01:01', '2021-09-07 10:21:02', 84),
(1006, 9001, '2021-09-01 12:11:01', '2021-09-01 12:31:01', 89),
(1006, 9002, '2021-09-06 10:01:01', '2021-09-06 10:21:01', 81),
(1005, 9002, '2021-09-05 10:01:01', '2021-09-05 10:21:01', 81),
(1005, 9001, '2021-09-05 10:31:01', '2021-09-05 10:51:01', 81),
(1004, 9002, '2021-09-05 10:01:01', '2021-09-05 10:21:01', 71),
(1004, 9001, '2021-09-05 10:31:01', '2021-09-05 10:51:01', 91),
(1004, 9002, '2021-09-05 10:01:01', '2021-09-05 10:21:01', 80),
(1004, 9001, '2021-09-05 10:31:01', '2021-09-05 10:51:01', 80);
 
select uid,score from exam_record;
1. 基本语法
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<窗口函数> OVER ([PARTITION BY <列名清单>] ORDER BY <排序列名清单> [rows between 开始位置 and 结束位置])

其中:

<窗口函数>:指需要使用的分析函数,如row_number()、sum()等。

over() : 用来指定函数执行的窗口范围,这个数据窗口大小可能会随着行的变化而变化;

如果括号中什么都不写,则意味着窗口包含满足WHERE条件的所有行,窗口函数基于所有行进行计算。如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select 
    uid,
    score,
    sum(score) over() as sum_score
from exam_record 

结果:

sum(score) over() as sum_score 会聚合所有的数据,将结果接到每一行的后面(窗口函数不会改变结果原表行数)。

2. 设置窗口的方法

如果不为空,则支持以下4中语法来设置窗口。

1)window_name

给窗口指定一个别名。如果SQL中涉及的窗口较多,采用别名可以看起来更清晰易读,如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
--给窗口指定别名:WINDOW my_window_name AS (PARTITION BY uid ORDER BY score)
SELECT
    uid,
    score,
    rank() OVER my_window_name AS rk_num,
    row_number() OVER my_window_name AS row_num
FROM exam_record
WINDOW my_window_name AS (PARTITION BY uid ORDER BY score)

2)partition by 子句

窗口按照哪些字段进行分组,窗口函数在不同的分组上分别执行,如:

实例1:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    sum(score) OVER(PARTITION BY uid) AS sum_score
FROM exam_record

结果:

sum(score) OVER(PARTITION BY uid) AS sum_score 会按照 uid 分组,分别求和,展示在每个分组的末尾。

如果我想看某个uid有多少行记录,并标明序号该如何实现?使用序号函数row_number()请看:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    row_number() OVER(PARTITION BY uid) AS row_num
FROM exam_record

可以看到,row_number()按照uid分组并从上到下按照顺序标号。我们看到1004中的score是无序的,如果想按照score降序排名应该怎么做呢?(实际场景:成绩排名)

可以结合 order by 子句实现

3)order by子句

按照哪些字段进行排序,窗口函数将按照排序后的记录顺序进行编号,如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    row_number() OVER(PARTITION BY uid ORDER BY score desc) AS row_num
FROM exam_record

这样就实现了每个uid内的分数降序排名,order by 后面可以跟多个列名,大家可以试一试。

当order by 与聚合类函数连用时,特别需要注意理解,如下面几个例子:

先看前面的例子,单独使用 partition by uid

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    sum(score) OVER(PARTITION BY uid) AS sum_score
FROM exam_record

结果:

单独使用order by uid

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    sum(score) OVER(ORDER BY uid) AS sum_score
FROM exam_record

结果:

注意观察uid 从1004->1005的变化,两条SQL的结果有什么不同:

  • partition by 按照uid分组,分别对score求和,”接到每一行的末尾“
    • 分组内求和,分组间相互独立。
  • order by 按照uid排序,对”序号“相同的元素进行求和,不同”序号“的数累加求和
    • 如果将”序号“认为是分组的话,个人理解这是一个分组求和并累加的过程
    • 即分组内求和,分组间累加。

再看,order by score 的例子

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    sum(score) OVER(ORDER BY score) AS sum_score
FROM exam_record

结果:

总结一下:

如果使⽤环境为hive,over()开窗函数前分排序函数和聚合函数两种。

当为排序函数,如row_number(),rank()等时,over中的order by只起到窗⼝内排序作⽤。

当为聚合函数,如max,min,count等时,over中的order by不仅起到窗⼝内排序,还起到窗⼝内从当前⾏到之前所有⾏的聚合(多了⼀个范围)。

4)rows 指定窗口大小

a.先看个例子,按照顺序,求score的平均值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    avg(score) OVER(ORDER BY score desc) AS avg_score
FROM exam_record

注意score相同的部分:

如果想要sql先按照score降序排列,每一行计算从第一行到当前行的score平均值,该怎么计算呢?——想办法做一个不重复的key

实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    row_score,
    avg(score) OVER(ORDER BY row_score) AS avg_score
FROM (
    SELECT
        uid,
        score,
        row_number() OVER(ORDER BY score desc) AS row_score
    FROM exam_record
) res

现在改下需求,希望求"我与前两名的平均值"应该怎么实现呢?

分析一下,"我与前两名"指的是当前行以及前两行总共三行数据求平均,也就是说,我们需要限定窗口的范围或者窗口大小。

b.引入窗口框架

指定窗口大小,又称为窗口框架。框架是对窗口进行进一步分区,框架有两种范围限定方式:

一种是使用 ROWS 子句,通过指定当前行之前或之后的固定数目的行来限制分区中的行数。

另一种是使用 RANGE 子句,按照排列序列的当前值,根据相同值来确定分区中的行数。

语法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ORDER BY 字段名 RANGE|ROWS 边界规则0 | [BETWEEN 边界规则1 AND 边界规则2]

RANGE | ROWS的区别是什么?

RANGE表示按照值的范围进行范围的定义,而ROWS表示按照行的范围进行范围的定义;边界规则的可取值见下表:

需要注意:

  • 当使用框架时,必须要有 order by 子句,如果仅指定了order by 子句而未指定框架,那么默认框架将采用 RANGE UNBOUNDED PRECEDING AND CURRENT ROW(表示当前行以及一直到第一行的数据)。
  • 如果窗口函数没有指定 order by 子句,也就不存在 ROWS/RANGE 窗口的计算。
  • PS: RANGE 只支持使用 UNBOUNDED 和 CURRENT ROW 窗口框架分隔符。

OK,回到前面的需求:求"我与前两名的平均值"。因为要"我与前两名",所以我们会用到规则 2 PRECEDING

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
之前2行的记录
之前1行的记录
自身(当前记录)
 
SELECT
    uid,
    score,
    avg(score) OVER(ORDER BY score desc rows 2 PRECEDING) AS avg_score
FROM exam_record

如果要求当前行及前后一行呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
之前1行的记录
自身(当前记录)
之后1行的记录
 
SELECT
    uid,
    score,
    avg(score) OVER(ORDER BY score desc rows between 1 PRECEDING and 1 FOLLOWING) AS avg_score
FROM exam_record

结果略~

其他组合举例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1.第一行到当前行
ORDER BY score desc rows UNBOUNDED PRECEDING
 
2.第一行到前一行(不含当前行)
ORDER BY score desc rows between UNBOUNDED PRECEDING and 1 PRECEDING 
 
3.第一行到后一行(包含当前行)
ORDER BY score desc rows between UNBOUNDED PRECEDING and 1 FOLLOWING
 
4.当前行到最后一行
ORDER BY score desc rows between CURRENT ROW and UNBOUNDED FOLLOWING 
注意,这种写法是错误的
ORDER BY score desc rows UNBOUNDED FOLLOWING -- 错误示范
 
5.前一行到最后一行(包含当前行)
ORDER BY score desc rows between 1 PRECEDING and UNBOUNDED FOLLOWING
 
6.后一行到最后一行(不含当前行)
ORDER BY score desc rows between 1 FOLLOWING and UNBOUNDED FOLLOWING
 
7.前一行到后一行(包含当前行) 
ORDER BY score desc rows between 1 PRECEDING and 1 FOLLOWING 
3. 开窗函数中加order by 和 不加 order by的区别

如果使⽤环境为hive,over()开窗函数前分排序函数和聚合函数两种。

当为排序函数,如row_number(),rank()等时,over中的order by只起到窗⼝内排序作⽤。

当为聚合函数,如max,min,count等时,over中的order by不仅起到窗⼝内排序,还起到窗⼝内从当前⾏到之前所有⾏的聚合(多了⼀个范围)。

如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-- sql ①
select id, dept, salary, min(salary) over(partition by dept) min_sal from dept;  
 
-- sql ②
select id, dept, salary, min(salary) over(partition by dept order by id) min_sal from dept; 

上⾯①②中的min_salary字段的值会不⼀样,原因是②中使⽤order by后,等同于 min(salary) over(partition by dept order by userid range between unbounded preceding and current row ),当然可以在order by后使⽤框架⼦句,即rows,range等,如果没有写框架⼦句,就默认在窗⼝范围中当前⾏到之前所有⾏的数据进⾏统计。

再看个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# 表数据为:exam_record(uid,exam_id,start_time,end_time,score)
# (1001, 9001, '2021-09-01 09:01:01', '2021-09-01 09:31:00', 100),
# (1001, 9001, '2021-09-02 09:01:01', '2021-09-01 09:31:00', 100),
# (1001, 9001, '2021-09-03 09:01:01', '2021-09-01 09:31:00', 100),
# (1002, 9001, '2021-09-01 09:01:01', '2021-09-01 09:31:00', 100),
# (1002, 9001, '2021-09-01 09:01:01', '2021-09-01 09:31:00', 100),
# (1002, 9001, '2021-09-02 09:01:01', '2021-09-01 09:31:00', 100); 
-- 执行下面的sql
select 
    uid,
    exam_id,
    start_time,
    sum(score) over(partition by uid) as one,
    sum(score) over(partition by uid order by start_time) as two
from exam_record

得到结果:

需要注意表中标注的部分

三、窗口函数用法举例

1. 序号函数:row_number() / rank() / dense_rank()(面试重点)

三者区别:

  • RANK(): 并列排序,跳过重复序号——1、1、3
  • ROW_NUMBER(): 顺序排序——1、2、3
  • DENSE_RANK(): 并列排序,不跳过重复序号——1、1、2
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
--给窗口指定别名:WINDOW my_window_name AS (PARTITION BY uid ORDER BY score)
SELECT
    uid,
    score,
    rank() OVER my_window_name AS rk_num,
    row_number() OVER my_window_name AS row_num
FROM exam_record
WINDOW my_window_name AS (PARTITION BY uid ORDER BY score)

我们来探索一下,如果不使用窗口函数,如何实现分数排序呢?(使用自连接的方法)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-- 相当于 rank()
SELECT 
    P1.uid,
    P1.score,
    (SELECT 
        COUNT(P2.score)
    FROM exam_record P2 
    WHERE P2.score > P1.score) + 1 AS rank_1
FROM exam_record P1
ORDER BY rank_1;

这里1234447..,如果想要7改为5呢,不跳过位次。相当于DENSE_RANK 函数。

只需要改 COUNT(P2.score) 为 COUNT(distinct P2.score) 即可。

2. 分布函数:percent_rank() / cume_dist()

1)percent_rank():

percent_rank()函数将某个数值在数据集中的排位作为数据集的百分比值返回,此处的百分比值的范围为 0 到 1。此函数可用于计算值在数据集内的相对位置。如班级成绩为例,返回的百分数30%表示某个分数排在班级总分排名的前30%。

每行按照公式(rank-1) / (rows-1)进行计算。其中,rank为RANK()函数产生的序号,rows为当前窗口的记录总行数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    rank() OVER my_window_name AS rank_num,
    PERCENT_RANK() OVER my_window_name AS prk
FROM exam_record
WINDOW my_window_name AS (ORDER BY score desc)

2)cume_dist():

如果按升序排列,则统计:小于等于当前值的行数/总行数。

如果是降序排列,则统计:大于等于当前值的行数/总行数。

如:查询小于等于当前成绩(score)的比例。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    rank() OVER my_window_name AS rank_num,
    cume_dist() OVER my_window_name AS cume_dist_num
FROM exam_record
WINDOW my_window_name AS (ORDER BY score asc)
3. 前后函数 lag(expr,n,defval)、lead(expr,n,defval)(面试重点)

Lag()和Lead()分析函数可以在同一次查询中取出同一字段的前N行的数据(Lag)和后N行的数据(Lead)作为独立的列。

在实际应用当中,若要用到取今天和昨天的某字段差值时,Lag()和Lead()函数的应用就显得尤为重要。当然,这种操作可以用表的自连接实现,但是LAG()和LEAD()与left join、rightjoin等自连接相比,效率更高,SQL更简洁。下面我就对这两个函数做一个简单的介绍。

函数语法如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
lag( exp_str,offset,defval) over(partition by .. order by …)
 
lead(exp_str,offset,defval) over(partition by .. order by …)

其中

  • exp_str 是字段名
  • Offset 是偏移量,即是上1个或上N个的值,假设当前行在表中排在第5行,offset 为3,则表示我们所要找的数据行就是表中的第2行(即5-3=2)。
  • Defval 默认值,当两个函数取 上N 或者 下N 个值,当在表中从当前行位置向前数N行已经超出了表的范围时,lag() 函数将defval这个参数值作为函数的返回值,若没有指定默认值,则返回NULL,那么在数学运算中,总要给一个默认值才不会出错。

用途:

  • 返回位于当前行的前n行的expr的值:LAG(expr,n)
  • 返回位于当前行的后n行的expr的值:LEAD(expr,n)

举例:查询前1名同学及后一名同学的成绩和当前同学成绩的差值(只排分数,不按uid分组)

先将前一名和后一名的分数与当前行的分数放在一起:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    LAG(score,1,0) OVER my_window_name AS `前一名分数`,
    LEAD(score,1,0) OVER my_window_name AS `后一名分数`
FROM exam_record
WINDOW my_window_name AS (ORDER BY score desc)

再做diff:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
    uid,
    score,
    score - `前一名分数` AS `与前一名分差`,
    score - `后一名分数` AS `与后一名分差`
FROM (
    SELECT
        uid,
        score,
        LAG(score,1,0) OVER my_window_name AS `前一名分数`,
        LEAD(score,1,0) OVER my_window_name AS `后一名分数`
    FROM exam_record
    WINDOW my_window_name AS (ORDER BY score desc)
) res
4. 头尾函数:FIRST_VALUE(expr),LAST_VALUE(expr)

用途:

  • 返回第一个expr的值:FIRST_VALUE(expr)
  • 返回最后一个expr的值:LAST_VALUE(expr)

应用场景:截止到当前成绩,按照分数排序查询第1个和最后1个的分数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    SELECT
        uid,
        score,
        FIRST_VALUE(score) OVER my_window_name AS `第一行分数`,
        LAST_VALUE(score) OVER my_window_name AS `最后一行分数`
    FROM exam_record
    WINDOW my_window_name AS (ORDER BY score desc)
5. 聚合函数+窗口函数联合使用

聚合函数也可以用于窗口函数。

原因就在于窗口函数的执行顺序(逻辑上的)是在FROM,JOIN,WHERE,GROUP BY,HAVING之后,在ORDER BY,LIMIT,SELECT DISTINCT之前。它执行时GROUP BY的聚合过程已经完成了,所以不会再产生数据聚合。

注:窗口函数是在where之后执行的,所以如果where子句需要用窗口函数作为条件,需要多一层查询,在子查询外面进行

前面基本用法中已经有部分举例,如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    SELECT
        uid,
        score,
        sum(score) OVER my_window_name AS sum_score,
        max(score) OVER my_window_name AS max_score,
        min(score) OVER my_window_name AS min_score,
        avg(score) OVER my_window_name AS avg_score
    FROM exam_record
    WINDOW my_window_name AS (ORDER BY score desc)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mysql> SELECT 
    ->     stu_id, 
    ->     lesson_id, 
    ->     score, 
    ->     create_time,
    ->     FIRST_VALUE(score) OVER w AS first_score, -- 按照lesson_id分区,create_time升序,取第一个score值
    ->     LAST_VALUE(score) OVER w AS last_score -- 按照lesson_id分区,create_time升序,取最后一个score值
    -> FROM t_score
    -> WHERE lesson_id IN ('L001','L002')
    -> WINDOW w AS (PARTITION BY lesson_id ORDER BY create_time)
    -> ;
+--------+-----------+-------+-------------+-------------+------------+
| stu_id | lesson_id | score | create_time | first_score | last_score |
+--------+-----------+-------+-------------+-------------+------------+
|      3 | L001      |   100 | 2018-08-07  |         100 |        100 |
|      1 | L001      |    98 | 2018-08-08  |         100 |         98 |
|      2 | L001      |    84 | 2018-08-09  |         100 |         99 |
|      4 | L001      |    99 | 2018-08-09  |         100 |         99 |
|      3 | L002      |    91 | 2018-08-07  |          91 |         91 |
|      1 | L002      |    86 | 2018-08-08  |          91 |         86 |
|      2 | L002      |    90 | 2018-08-09  |          91 |         90 |
|      4 | L002      |    88 | 2018-08-10  |          91 |         88 |
+--------+-----------+-------+-------------+-------------+------------+

四、面试题

1. 用户行为分析

表1:用户行为表tracking_log,大概字段有(user_id‘用户编号’,opr_id‘操作编号’,log_time‘操作时间’)如下所示:

问题:

1)统计每天符合以下条件的用户数:A操作之后是B操作,AB操作必须相邻

分析: (1)统计每天,所以需要按天分组统计求和 (2)A操作之后是B,且AB操作必须相邻,那就涉及一个前后问题,所以想到用窗口函数中的lag()或lead()。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-- 使用 lead() 实现 
select 
    dt,
    count(1) as res_cnt
from (
    select
        user_id,
        date_format(log_time,"%Y%m%d") as dt,
        opr_id as curr_opr, -- 当前操作
        lead(opr_id,1) over(partition by user_id,date_format(log_time,"%Y%m%d") order by log_time) as next_opr -- 获取 下一个操作
    from tracking_log
) res 
where curr_opr = "A" and next_opr="B" 
group by dt
 
---------------------------------
 
-- 使用 lag() 实现 
select 
    dt,
    count(1) as res_cnt
from (
    select
        user_id,
        date_format(log_time,"%Y%m%d") as dt,
        opr_id as curr_opr, -- 当前操作
        lag(opr_id,1) over(partition by user_id,date_format(log_time,"%Y%m%d") order by log_time) as before_opr -- 获取 前一个操作
    from tracking_log
) res 
where before_opr = "A" and curr_opr="B" 
group by dt

2)统计用户行为序列为A-B-D的用户数,其中:A-B之间可以有任何其他浏览记录(如C,E等),B-D之间除了C记录可以有任何其他浏览记录(如A,E等)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select 
    count(*)
from(
    select 
        user_id,
        group_concat(opr_id) ubp -- 先按照用户分组,将组内的opr_id拼接起来
    from tracking_log
    group by user_id
) a
where ubp like '%A%B%D%' and ubp not like '%A%B%C%D%'
2. 学生成绩分析

表:Enrollments (student_id, course_id) 是该表的主键。

1)查询每位学生获得的最高成绩和它所对应的科目,若科目成绩并列,取 course_id 最小的一门。查询结果需按 student_id 增序进行排序。

分析:因为需要最高成绩和所对应的科目,所以可采用窗口函数排序分组取第一个

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select 
 student_id,
 course_id,
 grade
from (
 select
  student_id,
  course_id,
  grade,
  row_number() over(partition by student_id order by grade desc,course_id asc) as rank_num 
 from Enrollments 
) res 
where rank_num = 1 
order by student_id 

解法2:IN 解法

取成绩在最大成绩之中的学生的最小课程号的课程

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select student_id,min(course_id)
from Enrollments
where (student_id,grade) in (
                     -- 先取最大成绩
      select student_id,max(grade)
      from Enrollments
      group by student_id)
group by student_id
order by student_id;

2)查询每一科目成绩最高和最低分数的学生,输出course_id,student_id,score

我们可以按科目查找成绩最高的同学和最低分的同学,然后利用union连接起来

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select 
 c_id,
 s_id
from(
 select 
  *,
  row_number() over(partition by c_id order by s_score desc) r
 from score
) a
where r = 1
 
union
 
select 
 c_id,
 s_id
from(
 select 
  *,
  row_number() over(partition by c_id order by s_score) r
 from score
) a
where r = 1;

解法2:case-when

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select 
     c_id,
     max(case when r1 = 1 then s_id else null end) '最高分学生',
     max(case when r2 = 1 then s_id else null end) '最低分学生'
from(
     select 
          *,
          row_number() over(partition by c_id order by s_score desc) r1,
  row_number() over(partition by c_id order by s_score) r2
 from score
) a
group by c_id;
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-04-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 浪尖聊大数据 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
枚举很好用啊,为啥阿里不建议返回值用枚举?
从手册可以看出,定义和使用枚举,阿里开发手册都是支持的,但是为啥,返回值就要反对了呢?
芋道源码
2019/12/24
2K0
枚举很好用啊,为啥阿里不建议返回值用枚举?
求你了,不要再在对外接口中使用枚举类型了!
最近,我们的线上环境出现了一个问题,线上代码在执行过程中抛出了一个IllegalArgumentException,分析堆栈后,发现最根本的的异常是以下内容:
Java识堂
2020/12/16
8840
求你了,不要再在对外接口中使用枚举类型了!
阿里巴巴Java开发手册(华山版)
前 言 《Java 开发手册》是阿里巴巴集团技术团队的集体智慧结晶和经验总结, 经历了多次大规模一线实战的检验及不断完善, 公开到业界后,众多社区开发者踊跃参与,共同打磨完善, 系统化地整理成册。现代软件行业的高速发展对开发者的综合素质要求越来越高, 因为不仅是编程知识点,其它维度的知识点也会影响到软件的最终交付质量。比如: 数据库的表结构和索引设计缺陷可能带来软件上的架构缺陷或性能风险; 工程结构混乱导致后续维护艰难;没有鉴权的漏洞代码易被黑客 攻击等等。所以本手册以 Java 开发者为中心视角,划分为编程规约、异常日志、 单元测试、 安全规约、 MySQL 数据库、 工程结构、 设计规约七个维度,再根据内容特征,细分成若干二级子目录。 另外,依据约束力强弱及故障敏感性,规约依次分为强制、推荐、参考三大类。 在延伸信息中,“说明” 对规约做了适当扩展和解释; “正例”提倡什么样的编码和实现方式; “反例”说明需要提防的雷区, 以及真实的错误案例。 手册的愿景是码出高效, 码出质量。 现代软件架构的复杂性需要协同开发完成, 如何高效地协同呢? 无规矩不成方圆,无规范难以协同, 比如, 制订交通法规表面上是要限制行车权, 实际上是保障公众的人身安全, 试想如果没有限速,没有红绿灯,谁还敢上路行驶? 对软件来说,适当的规范和标准绝不是消灭代码内容的创造性、优雅性,而是限制过度个性化,以一种普遍认可的统一方式一起做事,提升协作效率, 降低沟通成本。 代码的字里行间流淌的是软件系统的血液, 质量的提升是尽可能少踩坑,杜绝踩重复的坑, 切实提升系统稳定性, 码出质量。 代码规约扫描引擎。 次年,发布 36 万字的配套详解图书《码出高效》 , 本书秉持“图胜于表,表胜于言” 的理念,深入浅出地将计算机基础、面向对象思想、 JVM 探源、数据结构与集合、并发与多线程、单元测试等知识客观、立体地呈现出来。紧扣学以致用、学以精进的目标,结合阿里巴巴实践经验和故障案例,与底层源码解析融会贯通,娓娓道来。 此书所得收入均捐赠公益事情,希望用技术情怀帮助更多的人。 一、 编程规约 (一) 命名风格
全栈程序员站长
2022/11/15
1.2K0
25. Groovy 孵化功能-记录类record和密封sealed的学习
本篇内容为Groovy学习笔记第二十五篇。主要内容为Groovy孵化功能(incubating)的学习。
zinyan.com
2023/02/23
1K0
25. Groovy 孵化功能-记录类record和密封sealed的学习
开发规约:接口统一返回值格式 [resend]
年过完,又开始搞钱了,没啥可以帮大家的。重要的事情说三次:有换工作的,如果接的自己还不错的,可以私聊内推。一、前言本篇主要介绍一种常用的返回值格式以及详细说明。二、Dubbo 接口统一返回值格式我们在应用中经常会涉及到 server 和 client 的交互,目前比较流行的是基于 json 格式的数据交互。但是 json 只是消息的格式,其中的内容还需要我们自行设计。不管是 HTTP 接口还是 RPC 接口保持返回值格式统一很重要,这将大大降低 client 的开发成本。2.1 定义返回值四要素
BUG弄潮儿
2022/03/08
7010
开发规约:接口统一返回值格式 [resend]
优秀的开发者从命名开始
如何码出高质量的代码呢?其实命名也没有那么难,关键看你重不重视,愿不愿意花时间。以下是课程笔记和阿里巴巴的开发手册中觉得适用的部分,分享出来。
李明成
2020/04/01
4180
Java--Enum的思考
枚举类是Java5引进的特性,其目的是替换int枚举模式或者String枚举模式,使得语义更加清晰,另外也解决了行为和枚举绑定的问题.
屈定
2018/09/27
1K0
dubbo服务接口设计的几个建议
那如何解决呢?其实很简单。服务接口的参数类型最好是封装类,增加参数的话只是在这个类增加一个字段。示例如下:
用户7634691
2020/08/10
2K0
阿里Java编程规约【九】 注释规约
1.【强制】类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /** 内容 */ 格式,不得使用 // xxx 方式。 说明:在 IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应注释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。
acc8226
2022/05/17
1K0
java | 深入理解Java枚举类型(二)
blog.csdn.net/javazejian/article/details/71333103
JavaFish
2019/10/17
1.3K0
服务化最佳实践
建议将服务接口、服务模型、服务异常等均放在 API 包中,因为服务模型和异常也是 API 的一部分,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP)。
WindWant
2020/09/11
8150
Dubbo最佳实践,我整理了以下9点
Dubbo服务化,在当前互联网后端开发中,大部分都使用了Dubbo。截止目前github dubbo上,star也将近3万,使用dubbo的公司数量也很可观,Dubbo确实也是一个比较不错的服务化框架。
猿天地
2019/10/21
2.6K0
金秋十月,读阿里JAVA开发手册有感而发
最近重温阿里巴巴Java开发手册这本书,思考了什么样的代码是好代码,给大家分享一下我的想法,有哪里不对,欢迎指出,感激不尽。
捡田螺的小男孩
2020/04/15
4510
金秋十月,读阿里JAVA开发手册有感而发
万恶的NPE差点让我半个月工资没了
最近看到《阿里巴巴Java开发手册》(公众号回复[开发手册]免费获取)第11条规范写到:
java金融
2020/06/03
7380
万恶的NPE差点让我半个月工资没了
三歪问我为啥用枚举,枚举有哪些用法?
在学习过程中,我们也只是在定义常量的时候,会意识到枚举的存在,而定义常量其实可以在类中实现,这时就会感觉枚举有点鸡肋。但在实际项目开发的过程中,枚举因相当迷人的特性而受到越来越多的关注。
敖丙
2020/05/26
1.2K0
android阿里面试java基础锦集
接着上一篇 android阿里面试锦集 今天给大家带来一篇 android阿里面试java基础锦集。很多知识都是Thinking in Java上面的,所以如果要深入大家可以看看这本书。 1.重载函数的签名(区别是否是重载函数) 方法名+参数类型+参数顺序(返回值不是) 2.finalize的工作原理 一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存.所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清
何时夕
2018/05/02
7530
阿里编程规范 pdf_阿里前端开发规范
统一规范标准将有助于提高行业编码规范化水平,帮助行业人员提高开发质量和效率、大大降低代码维护成本
全栈程序员站长
2022/11/17
1.3K0
阿里编程规范 pdf_阿里前端开发规范
整理《阿里巴巴Java开发手册》常用的编码规约
1、抽象类命名使用Abstract或Base开头;异常类命名使用Exception结尾;测试类命名以它要测试的类的名称开始,以Test结尾。 2、中括号是数组类型的一部分,数组定义如下:String[] args; 3、POJO类中布尔类型的变量,都不要加is,否则部分框架解析会引起序列化错误。 4、包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。 5、如果使用到了设计模式,建议在类名中体现出具体模式。 6、接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的Javadoc注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量。 7、对于Service和DAO类,基于SOA的理念,暴露出来的服务一定是接口,内部的实现类用Impl的后缀与接口区别。 8、枚举类名建议带上Enum后缀,枚举成员名称需要全大写,单词间用下划线隔开。 9、各层命名规约:    A) Service/DAO层方法命名规约      1) 获取单个对象的方法用get做前缀。      2) 获取多个对象的方法用list做前缀。      3) 获取统计值的方法用count做前缀。      4) 插入的方法用save(推荐)或insert做前缀。      5) 删除的方法用remove(推荐)或delete做前缀。      6) 修改的方法用update做前缀。    B) 领域模型命名规约      1) 数据对象:xxxDO,xxx即为数据表名。      2) 数据传输对象:xxxDTO,xxx为业务领域相关的名称。      3) 展示对象:xxxVO,xxx一般为网页名称。      4) POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。
程序员云帆哥
2022/05/12
8030
整理《阿里巴巴Java开发手册》常用的编码规约
阿里Java编码手册实战详解-命名规范篇
1. 【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
JavaEdge
2020/02/29
1.2K0
阿里Java编码手册实战详解-命名规范篇
【Java数据结构】反射、枚举以及lambda表达式
Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。其本质是JVM得到Class对象之后,再通过Class对象进行反编译,从而获取对象的各种信息。
E绵绵
2024/09/12
1240
【Java数据结构】反射、枚举以及lambda表达式
推荐阅读
相关推荐
枚举很好用啊,为啥阿里不建议返回值用枚举?
更多 >
LV.0
这个人很懒,什么都没有留下~
目录
  • 一、窗口函数概述:
    • 1. 窗口函数的分类
    • 2. 窗口函数与普通聚合函数的区别:
  • 二、窗口函数的基本用法
    • 1. 基本语法
    • 2. 设置窗口的方法
    • 3. 开窗函数中加order by 和 不加 order by的区别
  • 三、窗口函数用法举例
    • 1. 序号函数:row_number() / rank() / dense_rank()(面试重点)
    • 2. 分布函数:percent_rank() / cume_dist()
    • 3. 前后函数 lag(expr,n,defval)、lead(expr,n,defval)(面试重点)
    • 4. 头尾函数:FIRST_VALUE(expr),LAST_VALUE(expr)
    • 5. 聚合函数+窗口函数联合使用
  • 四、面试题
    • 1. 用户行为分析
    • 2. 学生成绩分析
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档