1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-16 18:10:13 +08:00

Merge pull request #2077 from Davin-Lee/Davin-Lee-patch-1

Add sql-questions2,3,4,5
This commit is contained in:
Guide 2023-07-13 23:42:01 +08:00 committed by GitHub
commit 0b2f8bd56b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 3595 additions and 0 deletions

View File

@ -0,0 +1,448 @@
---
title: SQL常见面试题总结2
category: 数据库
tag:
- 数据库基础
- SQL
---
> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240)
## 增删改操作
SQL 插入记录的方式汇总:
- **普通插入(全字段)** `INSERT INTO table_name VALUES (value1, value2, ...)`
- **普通插入(限定字段)** `INSERT INTO table_name (column1, column2, ...) VALUES (value1, value2, ...)`
- **多条一次性插入** `INSERT INTO table_name (column1, column2, ...) VALUES (value1_1, value1_2, ...), (value2_1, value2_2, ...), ...`
- **从另一个表导入** `INSERT INTO table_name SELECT * FROM table_name2 [WHERE key=value]`
- **带更新的插入** `REPLACE INTO table_name VALUES (value1, value2, ...)`(注意这种原理是检测到主键或唯一性索引键重复就删除原记录后重新插入)
### 插入记录(一)
**描述**:牛客后台会记录每个用户的试卷作答记录到 `exam_record` 表,现在有两个用户的作答记录详情如下:
- 用户 1001 在 2021 年 9 月 1 日晚上 10 点 11 分 12 秒开始作答试卷 9001并在 50 分钟后提交,得了 90 分;
- 用户 1002 在 2021 年 9 月 4 日上午 7 点 1 分 2 秒开始作答试卷 9002并在 10 分钟后退出了平台。
试卷作答记录表`exam_record`中,表已建好,其结构如下,请用一条语句将这两条记录插入表中。
| Filed | Type | Null | Key | Extra | Default | Comment |
| ----------- | ---------- | ---- | ---- | -------------- | ------- | -------- |
| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID |
| uid | int(11) | NO | | | (NULL) | 用户 ID |
| exam_id | int(11) | NO | | | (NULL) | 试卷 ID |
| start_time | datetime | NO | | | (NULL) | 开始时间 |
| submit_time | datetime | YES | | | (NULL) | 提交时间 |
| score | tinyint(4) | YES | | | (NULL) | 得分 |
**答案**
```sql
// 存在自增主键,无需手动赋值
INSERT INTO exam_record (uid, exam_id, start_time, submit_time, score) VALUES
(1001, 9001, '2021-09-01 22:11:12', '2021-09-01 23:01:12', 90),
(1002, 9002, '2021-09-04 07:01:02', NULL, NULL);
```
### 插入记录(二)
**描述**:现有一张试卷作答记录表`exam_record`,结构如下表,其中包含多年来的用户作答试卷记录,由于数据越来越多,维护难度越来越大,需要对数据表内容做精简,历史数据做备份。
`exam_record`
| Filed | Type | Null | Key | Extra | Default | Comment |
| ----------- | ---------- | ---- | ---- | -------------- | ------- | -------- |
| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID |
| uid | int(11) | NO | | | (NULL) | 用户 ID |
| exam_id | int(11) | NO | | | (NULL) | 试卷 ID |
| start_time | datetime | NO | | | (NULL) | 开始时间 |
| submit_time | datetime | YES | | | (NULL) | 提交时间 |
| score | tinyint(4) | YES | | | (NULL) | 得分 |
我们已经创建了一张新表`exam_record_before_2021`用来备份 2021 年之前的试题作答记录,结构和`exam_record`表一致,请将 2021 年之前的已完成了的试题作答纪录导入到该表。
**答案**
```sql
INSERT INTO exam_record_before_2021 (uid, exam_id, start_time, submit_time, score)
SELECT uid,exam_id,start_time,submit_time,score
FROM exam_record
WHERE YEAR(submit_time) < 2021;
```
### 插入记录(三)
**描述**:现在有一套 ID 为 9003 的高难度 SQL 试卷,时长为一个半小时,请你将 2021-01-01 00:00:00 作为发布时间插入到试题信息表`examination_info`,不管该 ID 试卷是否存在,都要插入成功,请尝试插入它。
试题信息表`examination_info`
| Filed | Type | Null | Key | Extra | Default | Comment |
| ------------ | ----------- | ---- | ---- | -------------- | ------- | ------------ |
| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID |
| exam_id | int(11) | NO | UNI | | (NULL) | 试卷 ID |
| tag | varchar(32) | YES | | | (NULL) | 类别标签 |
| difficulty | varchar(8) | YES | | | (NULL) | 难度 |
| duration | int(11) | NO | | | (NULL) | 时长(分钟数) |
| release_time | datetime | YES | | | (NULL) | 发布时间 |
**答案**
```sql
REPLACE INTO examination_info VALUES
(NULL, 9003, "SQL", "hard", 90, "2021-01-01 00:00:00");
```
### 更新记录(一)
**描述:**现在有一张试卷信息表 `examination_info`, 表结构如下图所示:
| Filed | Type | Null | Key | Extra | Default | Comment |
| ------------ | -------- | ---- | ---- | -------------- | ------- | -------- |
| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID |
| exam_id | int(11) | NO | UNI | | (NULL) | 试卷 ID |
| tag | char(32) | YES | | | (NULL) | 类别标签 |
| difficulty | char(8) | YES | | | (NULL) | 难度 |
| duration | int(11) | NO | | | (NULL) | 时长 |
| release_time | datetime | YES | | | (NULL) | 发布时间 |
请把**examination_info**表中`tag``PYTHON``tag`字段全部修改为`Python`
**思路**:这题有两种解题思路,最容易想到的是直接`update + where`来指定条件更新,第二种就是根据要修改的字段进行查找替换
**答案一**
```sql
UPDATE examination_info SET tag = 'Python' WHERE tag='PYTHON'
```
**答案二**
```sql
UPDATE examination_info
SET tag = REPLACE(tag,'PYTHON','Python')
# REPLACE (目标字段,"查找内容","替换内容")
```
### 更新记录(二)
**描述**:现有一张试卷作答记录表 exam_record其中包含多年来的用户作答试卷记录结构如下表作答记录表 `exam_record` **`submit_time`** 为 完成时间 (注意这句话)
| Filed | Type | Null | Key | Extra | Default | Comment |
| ----------- | ---------- | ---- | ---- | -------------- | ------- | -------- |
| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID |
| uid | int(11) | NO | | | (NULL) | 用户 ID |
| exam_id | int(11) | NO | | | (NULL) | 试卷 ID |
| start_time | datetime | NO | | | (NULL) | 开始时间 |
| submit_time | datetime | YES | | | (NULL) | 提交时间 |
| score | tinyint(4) | YES | | | (NULL) | 得分 |
**题目要求**:请把 `exam_record` 表中 2021 年 9 月 1 日==之前==开始作答的==未完成==记录全部改为被动完成,即:将完成时间改为'2099-01-01 00:00:00',分数改为 0。
**思路:**注意题干中的关键字(已经高亮) `" xxx 时间 "`之前这个条件, 那么这里马上就要想到要进行时间的比较 可以直接 `xxx_time < "2021-09-01 00:00:00",` 也可以采用`date()`函数来进行比较;第二个条件就是 `"未完成"` 即完成时间为 NULL也就是题目中的提交时间 ----- `submit_time 为 NULL`
**答案:**
```sql
UPDATE exam_record SET submit_time = '2099-01-01 00:00:00', score = 0 WHERE DATE(start_time) < "2021-09-01" AND submit_time IS null
```
### 删除记录(一)
**描述**:现有一张试卷作答记录表 `exam_record`,其中包含多年来的用户作答试卷记录,结构如下表:
作答记录表`exam_record` **`start_time`** 是试卷开始时间`submit_time` 是交卷,即结束时间。
| Filed | Type | Null | Key | Extra | Default | Comment |
| ----------- | ---------- | ---- | ---- | -------------- | ------- | -------- |
| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID |
| uid | int(11) | NO | | | (NULL) | 用户 ID |
| exam_id | int(11) | NO | | | (NULL) | 试卷 ID |
| start_time | datetime | NO | | | (NULL) | 开始时间 |
| submit_time | datetime | YES | | | (NULL) | 提交时间 |
| score | tinyint(4) | YES | | | (NULL) | 得分 |
**要求**:请删除`exam_record`表中作答时间小于 5 分钟整且分数不及格(及格线为 60 分)的记录;
**思路**:这一题虽然是练习删除,仔细看确是考察对时间函数的用法,这里提及的分钟数比较,常用的函数有 **`TIMEDIFF`**和**`TIMESTAMPDIFF`** ,两者用法稍有区别,后者更为灵活,这都是看个人习惯。
1.  `TIMEDIFF`:两个时间之间的差值
```sql
TIMEDIFF(time1, time2)
```
两者参数都是必须的,都是一个时间或者日期时间表达式。如果指定的参数不合法或者是 NULL那么函数将返回 NULL。
对于这题而言,可以用在 minute 函数里面,因为 TIMEDIFF 计算出来的是时间的差值,在外面套一个 MINUTE 函数,计算出来的就是分钟数。
2. `TIMESTAMPDIFF`:用于计算两个日期的时间差
```sql
TIMESTAMPDIFF(unit,datetime_expr1,datetime_expr2)
# 参数说明
#unit: 日期比较返回的时间差单位,常用可选值如下:
SECOND
MINUTE分钟
HOUR小时
DAY
WEEK星期
MONTH
QUARTER季度
YEAR
# TIMESTAMPDIFF函数返回datetime_expr2 - datetime_expr1的结果人话 后面的 - 前面的 即2-1其中datetime_expr1和datetime_expr2可以是DATE或DATETIME类型值人话可以是“2023-01-01” 也可以是“2023-01-01- 00:00:00”
```
这题需要进行分钟的比较,那么就是 TIMESTAMPDIFF(MINUTE, 开始时间, 结束时间) < 5
**答案:**
```sql
DELETE FROM exam_record WHERE MINUTE (TIMEDIFF(submit_time , start_time)) < 5 AND score < 60
```
```sql
DELETE FROM exam_record WHERE TIMESTAMPDIFF(MINUTE, start_time, submit_time) < 5 AND score < 60
```
### 删除记录(二)
**描述**:现有一张试卷作答记录表`exam_record`,其中包含多年来的用户作答试卷记录,结构如下表:
作答记录表`exam_record``start_time` 是试卷开始时间,`submit_time` 是交卷时间,即结束时间,如果未完成的话,则为空。
| Filed | Type | Null | Key | Extra | Default | Comment |
| ----------- | ---------- | :--: | ---- | -------------- | ------- | -------- |
| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID |
| uid | int(11) | NO | | | (NULL) | 用户 ID |
| exam_id | int(11) | NO | | | (NULL) | 试卷 ID |
| start_time | datetime | NO | | | (NULL) | 开始时间 |
| submit_time | datetime | YES | | | (NULL) | 提交时间 |
| score | tinyint(4) | YES | | | (NULL) | 分数 |
**要求**:请删除`exam_record`表中未完成作答==或==作答时间小于 5 分钟整的记录中,开始作答时间最早的 3 条记录。
**思路**:这题比较简单,但是要注意题干中给出的信息,结束时间,如果未完成的话,则为空,这个其实就是一个条件
还有一个条件就是小于 5 分钟,跟上题类似,但是这里是**或**,即两个条件满足一个就行;另外就是稍微考察到了排序和 limit 的用法。
**答案**
```sql
DELETE FROM exam_record WHERE submit_time IS null OR TIMESTAMPDIFF(MINUTE, start_time, submit_time) < 5
ORDER BY start_time
LIMIT 3
# 默认就是asc desc是降序排列
```
### 删除记录(三)
**描述**:现有一张试卷作答记录表 exam_record其中包含多年来的用户作答试卷记录结构如下表
| Filed | Type | Null | Key | Extra | Default | Comment |
| ----------- | ---------- | :--: | ---- | -------------- | ------- | -------- |
| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID |
| uid | int(11) | NO | | | (NULL) | 用户 ID |
| exam_id | int(11) | NO | | | (NULL) | 试卷 ID |
| start_time | datetime | NO | | | (NULL) | 开始时间 |
| submit_time | datetime | YES | | | (NULL) | 提交时间 |
| score | tinyint(4) | YES | | | (NULL) | 分数 |
**要求**:请删除`exam_record`表中所有记录,==并重置自增主键==
**思路**:这题考察对三种删除语句的区别,注意高亮部分,要求重置主键;
- `DROP`: 清空表,删除表结构,不可逆
- `TRUNCATE`: 格式化表,不删除表结构,不可逆
- `DELETE`:删除数据,可逆
这里选用`TRUNCATE`的原因是TRUNCATE 只能作用于表;`TRUNCATE`会清空表中的所有行,但表结构及其约束、索引等保持不变;`TRUNCATE`会重置表的自增值;使用`TRUNCATE`后会使表和索引所占用的空间会恢复到初始大小。
这题也可以采用`DELETE`来做,但是在删除后,还需要手动`ALTER`表结构来设置主键初始值;
同理也可以采用`DROP`来做,直接删除整张表,包括表结构,然后再新建表即可。
**答案**
```sql
TRUNCATE exam_record;
```
## 表与索引操作
### 创建一张新表
**描述**:现有一张用户信息表,其中包含多年来在平台注册过的用户信息,随着牛客平台的不断壮大,用户量飞速增长,为了高效地为高活跃用户提供服务,现需要将部分用户拆分出一张新表。
原来的用户信息表:
| Filed | Type | Null | Key | Default | Extra | Comment |
| ------------- | ----------- | ---- | ---- | ----------------- | -------------- | -------- |
| id | int(11) | NO | PRI | (NULL) | auto_increment | 自增 ID |
| uid | int(11) | NO | UNI | (NULL) | | 用户 ID |
| nick_name | varchar(64) | YES | | (NULL) | | 昵称 |
| achievement | int(11) | YES | | 0 | | 成就值 |
| level | int(11) | YES | | (NULL) | | 用户等级 |
| job | varchar(32) | YES | | (NULL) | | 职业方向 |
| register_time | datetime | YES | | CURRENT_TIMESTAMP | | 注册时间 |
作为数据分析师,请**创建一张优质用户信息表 user_info_vip**,表结构和用户信息表一致。
你应该返回的输出如下表格所示,请写出建表语句将表格中所有限制和说明记录到表里。
| Filed | Type | Null | Key | Default | Extra | Comment |
| ------------- | ----------- | ---- | ---- | ----------------- | -------------- | -------- |
| id | int(11) | NO | PRI | (NULL) | auto_increment | 自增 ID |
| uid | int(11) | NO | UNI | (NULL) | | 用户 ID |
| nick_name | varchar(64) | YES | | (NULL) | | 昵称 |
| achievement | int(11) | YES | | 0 | | 成就值 |
| level | int(11) | YES | | (NULL) | | 用户等级 |
| job | varchar(32) | YES | | (NULL) | | 职业方向 |
| register_time | datetime | YES | | CURRENT_TIMESTAMP | | 注册时间 |
**思路:**如果这题给出了旧表的名称,可直接`create table 新表 as select * from 旧表;` 但是这题并没有给出旧表名称,所以需要自己创建,注意默认值和键的创建即可,比较简单。(注意:如果是在牛客网上面执行,请注意 comment 中要和题目中的 comment 保持一致,包括大小写,否则不通过,还有字符也要设置)
答案:
```sql
CREATE TABLE IF NOT EXISTS user_info_vip(
id INT(11) PRIMARY KEY AUTO_INCREMENT COMMENT'自增ID',
uid INT(11) UNIQUE NOT NULL COMMENT '用户ID',
nick_name VARCHAR(64) COMMENT'昵称',
achievement INT(11) DEFAULT 0 COMMENT '成就值',
level INT(11) COMMENT '用户等级',
job VARCHAR(32) COMMENT '职业方向',
register_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间'
)CHARACTER SET UTF8
```
### 修改表
**描述** 现有一张用户信息表`user_info`,其中包含多年来在平台注册过的用户信息。
**用户信息表 `user_info`**
| Filed | Type | Null | Key | Default | Extra | Comment |
| ------------- | ----------- | ---- | ---- | ----------------- | -------------- | -------- |
| id | int(11) | NO | PRI | (NULL) | auto_increment | 自增 ID |
| uid | int(11) | NO | UNI | (NULL) | | 用户 ID |
| nick_name | varchar(64) | YES | | (NULL) | | 昵称 |
| achievement | int(11) | YES | | 0 | | 成就值 |
| level | int(11) | YES | | (NULL) | | 用户等级 |
| job | varchar(32) | YES | | (NULL) | | 职业方向 |
| register_time | datetime | YES | | CURRENT_TIMESTAMP | | 注册时间 |
**要求:**请在用户信息表,字段 `level` 的后面增加一列最多可保存 15 个汉字的字段 `school`;并将表中 `job` 列名改为 `profession`,同时 `varchar` 字段长度变为 10`achievement` 的默认值设置为 0。
**思路:**首先做这题之前,需要了解 ALTER 语句的基本用法:
- 添加一列:`ALTER TABLE 表名 ADD COLUMN 列名 类型 【first | after 字段名】;`first 在某列之前添加after 反之)
- 修改列的类型或约束:`ALTER TABLE 表名 MODIFY COLUMN 列名 新类型 【新约束】;`
- 修改列名:`ALTER TABLE 表名 change COLUMN 旧列名 新列名 类型;`
- 删除列:`ALTER TABLE 表名 drop COLUMN 列名;`
- 修改表名:`ALTER TABLE 表名 rename 【to】 新表名;`
- 将某一列放到第一列:`ALTER TABLE 表名 MODIFY COLUMN 列名 类型 first;`
`COLUMN` 关键字其实可以省略不写,这里基于规范还是罗列出来了。
在修改时,如果有多个修改项,可以写到一起,但要注意格式
**答案:**
```sql
ALTER TABLE user_info
ADD school VARCHAR(15) AFTER level,
CHANGE job profession VARCHAR(10),
MODIFY achievement INT(11) DEFAULT 0;
```
### 删除表
**描述:**现有一张试卷作答记录表 `exam_record`,其中包含多年来的用户作答试卷记录。一般每年都会为 `exam_record` 表建立一张备份表 `exam_record_{YEAR}{YEAR}` 为对应年份。
现在随着数据越来越多存储告急请你把很久前的2011 到 2014 年)备份表都删掉(如果存在的话)。
**思路:**这题很简单,直接删就行,如果嫌麻烦,可以将要删除的表用逗号隔开,写到一行;这里肯定会有小伙伴问:如果要删除很多张表呢?放心,如果要删除很多张表,可以写脚本来进行删除。
**答案:**
```sql
DROP TABLE IF EXISTS exam_record_2011;
DROP TABLE IF EXISTS exam_record_2012;
DROP TABLE IF EXISTS exam_record_2013;
DROP TABLE IF EXISTS exam_record_2014;
```
### 创建索引
**描述:**现有一张试卷信息表 `examination_info`,其中包含各种类型试卷的信息。为了对表更方便快捷地查询,需要在 `examination_info` 表创建以下索引,
规则如下:在 `duration` 列创建普通索引 `idx_duration`、在 `exam_id` 列创建唯一性索引 `uniq_idx_exam_id`、在 `tag` 列创建全文索引 `full_idx_tag`
根据题意,将返回如下结果:
| examination_info | 0 | PRIMARY | 1 | id | A | 0 | | | | BTREE |
| ---------------- | ---- | ---------------- | ---- | -------- | ---- | ---- | ---- | ---- | ---- | -------- |
| examination_info | 0 | uniq_idx_exam_id | 1 | exam_id | A | 0 | | | YES | BTREE |
| examination_info | 1 | idx_duration | 1 | duration | A | 0 | | | | BTREE |
| examination_info | 1 | full_idx_tag | 1 | tag | | 0 | | | YES | FULLTEXT |
备注:后台会通过 `SHOW INDEX FROM examination_info` 语句来对比输出结果
**思路:**做这题首先需要了解常见的索引类型:
- B-Tree 索引B-Tree或称为平衡树索引是最常见和默认的索引类型。它适用于各种查询条件可以快速定位到符合条件的数据。B-Tree 索引适用于普通的查找操作,支持等值查询、范围查询和排序。
- 唯一索引:唯一索引与普通的 B-Tree 索引类似不同之处在于它要求被索引的列的值是唯一的。这意味着在插入或更新数据时MySQL 会验证索引列的唯一性。
- 主键索引:主键索引是一种特殊的唯一索引,它用于唯一标识表中的每一行数据。每个表只能有一个主键索引,它可以帮助提高数据的访问速度和数据完整性。
- 全文索引:全文索引用于在文本数据中进行全文搜索。它支持在文本字段中进行关键字搜索,而不仅仅是简单的等值或范围查找。全文索引适用于需要进行全文搜索的应用场景。
```sql
-- 示例:
-- 添加B-Tree索引
CREATE INDEX idx_name(索引名) ON 表名 (字段名); -- idx_name为索引名以下都是
-- 创建唯一索引:
CREATE UNIQUE INDEX idx_name ON 表名 (字段名);
-- 创建一个主键索引:
ALTER TABLE 表名 ADD PRIMARY KEY (字段名);
-- 创建一个全文索引
ALTER TABLE 表名 ADD FULLTEXT INDEX idx_name (字段名);
-- 通过以上示例可以看出create 和 alter 都可以添加索引
```
有了以上的基础知识之后,该题答案也就浮出水面了。
**答案:**
```sql
ALTER TABLE examination_info
ADD INDEX idx_duration(duration),
ADD UNIQUE INDEX uniq_idx_exam_id(exam_id),
ADD FULLTEXT INDEX full_idx_tag(tag);
```
### 删除索引
**描述:**请删除`examination_info`表上的唯一索引 uniq_idx_exam_id 和全文索引 full_idx_tag。
**思路:**该题考察删除索引的基本语法:
```sql
-- 使用 DROP INDEX 删除索引
DROP INDEX idx_name ON 表名;
-- 使用 ALTER TABLE 删除索引
ALTER TABLE employees DROP INDEX idx_email;
```
这里需要注意的是:在 MySQL 中,一次删除多个索引的操作是不支持的。每次删除索引时,只能指定一个索引名称进行删除。
而且 **DROP** 命令需要慎用!!!
**答案:**
```sql
DROP INDEX uniq_idx_exam_id ON examination_info;
DROP INDEX full_idx_tag ON examination_info;
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,831 @@
---
title: SQL常见面试题总结4
category: 数据库
tag:
- 数据库基础
- SQL
---
> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240)
较难或者困难的题目可以根据自身实际情况和面试需要来决定是否要跳过。
## 专用窗口函数
MySQL 8.0 版本引入了窗口函数的支持,下面是 MySQL 中常见的窗口函数及其用法:
1. `ROW_NUMBER()`: 为查询结果集中的每一行分配一个唯一的整数值。
```sql
SELECT col1, col2, ROW_NUMBER() OVER (ORDER BY col1) AS row_num
FROM table;
```
2. `RANK()`: 计算每一行在排序结果中的排名。
```sql
SELECT col1, col2, RANK() OVER (ORDER BY col1 DESC) AS ranking
FROM table;
```
3. `DENSE_RANK()`: 计算每一行在排序结果中的排名,保留相同的排名。
```sql
SELECT col1, col2, DENSE_RANK() OVER (ORDER BY col1 DESC) AS ranking
FROM table;
```
4. `NTILE(n)`: 将结果分成 n 个基本均匀的桶,并为每个桶分配一个标识号。
```sql
SELECT col1, col2, NTILE(4) OVER (ORDER BY col1) AS bucket
FROM table;
```
5. `SUM()`, `AVG()`,`COUNT()`, `MIN()`, `MAX()`: 这些聚合函数也可以与窗口函数结合使用,计算窗口内指定列的汇总、平均值、计数、最小值和最大值。
```sql
SELECT col1, col2, SUM(col1) OVER () AS sum_col
FROM table;
```
6. `LEAD()``LAG()`: LEAD 函数用于获取当前行之后的某个偏移量的行的值,而 LAG 函数用于获取当前行之前的某个偏移量的行的值。
```sql
SELECT col1, col2, LEAD(col1, 1) OVER (ORDER BY col1) AS next_col1,
LAG(col1, 1) OVER (ORDER BY col1) AS prev_col1
FROM table;
```
7. `FIRST_VALUE()``LAST_VALUE()`: FIRST_VALUE 函数用于获取窗口内指定列的第一个值LAST_VALUE 函数用于获取窗口内指定列的最后一个值。
```sql
SELECT col1, col2, FIRST_VALUE(col2) OVER (PARTITION BY col1 ORDER BY col2) AS first_val,
LAST_VALUE(col2) OVER (PARTITION BY col1 ORDER BY col2) AS last_val
FROM table;
```
窗口函数通常需要配合 OVER 子句一起使用,用于定义窗口的大小、排序规则和分组方式。
### 每类试卷得分前三名
**描述:**
现有试卷信息表 `examination_info``exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间):
| id | exam_id | tag | difficulty | duration | release_time |
| ---- | ------- | ---- | ---------- | -------- | ------------------- |
| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 |
| 2 | 9002 | SQL | hard | 60 | 2021-09-01 06:00:00 |
| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 |
试卷作答记录表 `exam_record``uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, score 得分):
| id | uid | exam_id | start_time | submit_time | score |
| ---- | ---- | ------- | ------------------- | ------------------- | ------ |
| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 78 |
| 2 | 1002 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 |
| 3 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 |
| 4 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 86 |
| 5 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:51 | 89 |
| 6 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 |
| 7 | 1005 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 |
| 8 | 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 84 |
| 9 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 |
| 10 | 1003 | 9002 | 2021-09-01 14:01:01 | (NULL) | (NULL) |
找到每类试卷得分的前 3 名,如果两人最大分数相同,选择最小分数大者,如果还相同,选择 uid 大者。由示例数据结果输出如下:
| tid | uid | ranking |
| ---- | ---- | ------- |
| SQL | 1003 | 1 |
| SQL | 1004 | 2 |
| SQL | 1002 | 3 |
| 算法 | 1005 | 1 |
| 算法 | 1006 | 2 |
| 算法 | 1003 | 3 |
**解释:**有作答得分记录的试卷 tag 有 SQL 和算法SQL 试卷用户 1001、1002、1003、1004 有作答得分,最高得分分别为 81、81、89、85最低得分分别为 78、81、86、40因此先按最高得分排名再按最低得分排名取前三为 1003、1004、1002。
**答案:**
```sql
SELECT tag,
UID,
ranking
FROM
(SELECT b.tag AS tag,
a.uid AS UID,
ROW_NUMBER() OVER (PARTITION BY b.tag
ORDER BY b.tag,
max(a.score) DESC,
min(a.score) DESC,
a.uid DESC) AS ranking
FROM exam_record a
LEFT JOIN examination_info b ON a.exam_id = b.exam_id
GROUP BY b.tag,
a.uid) t
WHERE ranking <= 3
```
### 第二快/慢用时之差大于试卷时长一半的试卷(较难)
**描述:**
现有试卷信息表 `examination_info``exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间):
| id | exam_id | tag | difficulty | duration | release_time |
| ---- | ------- | ---- | ---------- | -------- | ------------------- |
| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 |
| 2 | 9002 | C++ | hard | 60 | 2021-09-01 06:00:00 |
| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 |
试卷作答记录表 `exam_record``uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分):
| id | uid | exam_id | start_time | submit_time | score |
| ---- | ---- | ------- | ------------------- | ------------------- | ------ |
| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:51:01 | 78 |
| 2 | 1001 | 9002 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 |
| 3 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 |
| 4 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:59:01 | 86 |
| 5 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:51 | 89 |
| 6 | 1004 | 9002 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 |
| 7 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 |
| 8 | 1006 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 84 |
| 9 | 1003 | 9001 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 |
| 10 | 1003 | 9002 | 2021-09-01 14:01:01 | (NULL) | (NULL) |
| 11 | 1005 | 9001 | 2021-09-01 14:01:01 | (NULL) | (NULL) |
| 12 | 1003 | 9003 | 2021-09-08 15:01:01 | (NULL) | (NULL) |
找到第二快和第二慢用时之差大于试卷时长的一半的试卷信息,按试卷 ID 降序排序。由示例数据结果输出如下:
| exam_id | duration | release_time |
| ------- | -------- | ------------------- |
| 9001 | 60 | 2021-09-01 06:00:00 |
**解释**:试卷 9001 被作答用时有 50 分钟、50 分钟、30 分 1 秒、11 分钟、10 分钟,第二快和第二慢用时之差为 50 分钟-11 分钟=39 分钟,试卷时长为 60 分钟,因此满足大于试卷时长一半的条件,输出试卷 ID、时长、发布时间。
**思路:**
第一步,找到每张试卷完成时间的顺序排名和倒序排名 也就是表 a
第二步,与通过试卷信息表 b 建立内连接,并根据试卷 id 分组,利用`having`筛选排名为第二个数据,将秒转化为分钟并进行比较,最后再根据试卷 id 倒序排序就行
**答案:**
```sql
SELECT a.exam_id,
b.duration,
b.release_time
FROM
(SELECT exam_id,
row_number() OVER (PARTITION BY exam_id
ORDER BY timestampdiff(SECOND, start_time, submit_time) DESC) rn1,
row_number() OVER (PARTITION BY exam_id
ORDER BY timestampdiff(SECOND, start_time, submit_time) ASC) rn2,
timestampdiff(SECOND, start_time, submit_time) timex
FROM exam_record
WHERE score IS NOT NULL ) a
INNER JOIN examination_info b ON a.exam_id = b.exam_id
GROUP BY a.exam_id
HAVING (max(IF (rn1 = 2, a.timex, 0))- max(IF (rn2 = 2, a.timex, 0)))/ 60 > b.duration / 2
ORDER BY a.exam_id DESC
```
### 连续两次作答试卷的最大时间窗(较难)
**描述**
现有试卷作答记录表 `exam_record``uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分):
| id | uid | exam_id | start_time | submit_time | score |
| ---- | ---- | ------- | ------------------- | ------------------- | ----- |
| 1 | 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:02 | 84 |
| 2 | 1006 | 9001 | 2021-09-01 12:11:01 | 2021-09-01 12:31:01 | 89 |
| 3 | 1006 | 9002 | 2021-09-06 10:01:01 | 2021-09-06 10:21:01 | 81 |
| 4 | 1005 | 9002 | 2021-09-05 10:01:01 | 2021-09-05 10:21:01 | 81 |
| 5 | 1005 | 9001 | 2021-09-05 10:31:01 | 2021-09-05 10:51:01 | 81 |
请计算在 2021 年至少有两天作答过试卷的人中,计算该年连续两次作答试卷的最大时间窗 `days_window`,那么根据该年的历史规律他在 `days_window` 天里平均会做多少套试卷,按最大时间窗和平均做答试卷套数倒序排序。由示例数据结果输出如下:
| uid | days_window | avg_exam_cnt |
| ---- | ----------- | ------------ |
| 1006 | 6 | 2.57 |
**解释:**用户 1006 分别在 20210901、20210906、20210907 作答过 3 次试卷,连续两次作答最大时间窗为 6 天1 号到 6 号),他 1 号到 7 号这 7 天里共做了 3 张试卷,平均每天 3/7=0.428571 张,那么 6 天里平均会做 0.428571\*6=2.57 张试卷(保留两位小数);用户 1005 在 20210905 做了两张试卷,但是只有一天的作答记录,过滤掉。
**思路:**
上面这个解释中提示要对作答记录去重,千万别被骗了,不要去重!去重就通不过测试用例。注意限制时间是 2021 年;
而且要注意时间差要+1 天;还要注意==没交卷也算在内== (反正感觉这题描述不清,出的不是很好)
**答案:**
```sql
SELECT UID,
max(datediff(next_time, start_time)) + 1 AS days_window,
round(count(start_time)/(datediff(max(start_time), min(start_time))+ 1) * (max(datediff(next_time, start_time))+ 1), 2) AS avg_exam_cnt
FROM
(SELECT UID,
start_time,
lead(start_time, 1) OVER (PARTITION BY UID
ORDER BY start_time) AS next_time
FROM exam_record
WHERE YEAR (start_time) = '2021' ) a
GROUP BY UID
HAVING count(DISTINCT date(start_time)) > 1
ORDER BY days_window DESC,
avg_exam_cnt DESC
```
### 近三个月未完成为 0 的用户完成情况
**描述:**
现有试卷作答记录表 `exam_record``uid`:用户 ID, `exam_id`:试卷 ID, `start_time`:开始作答时间, `submit_time`:交卷时间,为空的话则代表未完成, `score`:得分):
| id | uid | exam_id | start_time | submit_time | score |
| ---- | ---- | ------- | ------------------- | ------------------- | ------ |
| 1 | 1006 | 9003 | 2021-09-06 10:01:01 | 2021-09-06 10:21:02 | 84 |
| 2 | 1006 | 9001 | 2021-08-02 12:11:01 | 2021-08-02 12:31:01 | 89 |
| 3 | 1006 | 9002 | 2021-06-06 10:01:01 | 2021-06-06 10:21:01 | 81 |
| 4 | 1006 | 9002 | 2021-05-06 10:01:01 | 2021-05-06 10:21:01 | 81 |
| 5 | 1006 | 9001 | 2021-05-01 12:01:01 | (NULL) | (NULL) |
| 6 | 1001 | 9001 | 2021-09-05 10:31:01 | 2021-09-05 10:51:01 | 81 |
| 7 | 1001 | 9003 | 2021-08-01 09:01:01 | 2021-08-01 09:51:11 | 78 |
| 8 | 1001 | 9002 | 2021-07-01 09:01:01 | 2021-07-01 09:31:00 | 81 |
| 9 | 1001 | 9002 | 2021-07-01 12:01:01 | 2021-07-01 12:31:01 | 81 |
| 10 | 1001 | 9002 | 2021-07-01 12:01:01 | (NULL) | (NULL) |
找到每个人近三个有试卷作答记录的月份中没有试卷是未完成状态的用户的试卷作答完成数,按试卷完成数和用户 ID 降序排名。由示例数据结果输出如下:
| uid | exam_complete_cnt |
| ---- | ----------------- |
| 1006 | 3 |
**解释:**用户 1006 近三个有作答试卷的月份为 202109、202108、202106作答试卷数为 3全部完成用户 1001 近三个有作答试卷的月份为 202109、202108、202107作答试卷数为 5完成试卷数为 4因为有未完成试卷故过滤掉。
**思路:**
1. `找到每个人近三个有试卷作答记录的月份中没有试卷是未完成状态的用户的试卷作答完成数`首先看这句话,肯定要先根据人进行分组
2. 最近三个月,可以采用连续重复排名,倒序排列,排名<=3
3. 统计作答数
4. 拼装剩余条件
5. 排序
**答案:**
```sql
SELECT UID,
count(score) exam_complete_cnt
FROM
(SELECT *, DENSE_RANK() OVER (PARTITION BY UID
ORDER BY date_format(start_time, '%Y%m') DESC) dr
FROM exam_record) t1
WHERE dr <= 3
GROUP BY UID
HAVING count(dr)= count(score)
ORDER BY exam_complete_cnt DESC,
UID DESC
```
### 未完成率较高的 50%用户近三个月答卷情况(困难)
**描述:**
现有用户信息表 `user_info``uid` 用户 ID`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间):
| id | uid | nick_name | achievement | level | job | register_time |
| ---- | ---- | ----------- | ----------- | ----- | ---- | ------------------- |
| 1 | 1001 | 牛客 1 号 | 3200 | 7 | 算法 | 2020-01-01 10:00:00 |
| 2 | 1002 | 牛客 2 号 | 2500 | 6 | 算法 | 2020-01-01 10:00:00 |
| 3 | 1003 | 牛客 3 号 ♂ | 2200 | 5 | 算法 | 2020-01-01 10:00:00 |
试卷信息表 `examination_info``exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间):
| id | exam_id | tag | difficulty | duration | release_time |
| ---- | ------- | ------ | ---------- | -------- | ------------------- |
| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 |
| 2 | 9002 | SQL | hard | 80 | 2020-01-01 10:00:00 |
| 3 | 9003 | 算法 | hard | 80 | 2020-01-01 10:00:00 |
| 4 | 9004 | PYTHON | medium | 70 | 2020-01-01 10:00:00 |
试卷作答记录表 `exam_record``uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分):
| id | uid | exam_id | start_time | submit_time | score |
| ---- | ---- | ------- | ------------------- | ------------------- | ----- |
| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 |
| 15 | 1002 | 9001 | 2020-01-01 18:01:01 | 2020-01-01 18:59:02 | 90 |
| 13 | 1001 | 9001 | 2020-01-02 10:01:01 | 2020-01-02 10:31:01 | 89 |
| 2 | 1002 | 9001 | 2020-01-20 10:01:01 | | |
| 3 | 1002 | 9001 | 2020-02-01 12:11:01 | | |
| 5 | 1001 | 9001 | 2020-03-01 12:01:01 | | |
| 6 | 1002 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:41:01 | 90 |
| 4 | 1003 | 9001 | 2020-03-01 19:01:01 | | |
| 7 | 1002 | 9001 | 2020-05-02 19:01:01 | 2020-05-02 19:32:00 | 90 |
| 14 | 1001 | 9002 | 2020-01-01 12:11:01 | | |
| 8 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 |
| 9 | 1001 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 |
| 10 | 1002 | 9002 | 2020-02-02 12:01:01 | | |
| 11 | 1002 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:43:01 | 81 |
| 12 | 1002 | 9002 | 2020-03-02 12:11:01 | | |
| 17 | 1001 | 9002 | 2020-05-05 18:01:01 | | |
| 16 | 1002 | 9003 | 2020-05-06 12:01:01 | | |
请统计 SQL 试卷上未完成率较高的 50%用户中6 级和 7 级用户在有试卷作答记录的近三个月中,每个月的答卷数目和完成数目。按用户 ID、月份升序排序。
由示例数据结果输出如下:
| uid | start_month | total_cnt | complete_cnt |
| ---- | ----------- | --------- | ------------ |
| 1002 | 202002 | 3 | 1 |
| 1002 | 202003 | 2 | 1 |
| 1002 | 202005 | 2 | 1 |
解释:各个用户对 SQL 试卷的未完成数、作答总数、未完成率如下:
| uid | incomplete_cnt | total_cnt | incomplete_rate |
| ---- | -------------- | --------- | --------------- |
| 1001 | 3 | 7 | 0.4286 |
| 1002 | 4 | 8 | 0.5000 |
| 1003 | 1 | 1 | 1.0000 |
1001、1002、1003 分别排在 1.0、0.5、0.0 的位置,因此较高的 50%用户(排位<=0.5)为 1002、1003
1003 不是 6 级或 7 级;
有试卷作答记录的近三个月为 202005、202003、202002
这三个月里 1002 的作答题数分别为 3、2、2完成数目分别为 1、1、1。
**思路:**
注意点:这题注意求的是所有的答题次数和完成次数,而 sql 类别的试卷是限制未完成率排名6, 7 级用户限制的是做题记录。
先求出未完成率的排名
```sql
SELECT UID,
count(submit_time IS NULL
OR NULL)/ count(start_time) AS num,
PERCENT_RANK() OVER (
ORDER BY count(submit_time IS NULL
OR NULL)/ count(start_time)) AS ranking
FROM exam_record
LEFT JOIN examination_info USING (exam_id)
WHERE tag = 'SQL'
GROUP BY UID
```
再求出最近三个月的练习记录
```sql
SELECT UID,
date_format(start_time, '%Y%m') AS month_d,
submit_time,
exam_id,
dense_rank() OVER (PARTITION BY UID
ORDER BY date_format(start_time, '%Y%m') DESC) AS ranking
FROM exam_record
LEFT JOIN user_info USING (UID)
WHERE LEVEL IN (6,7)
```
**答案:**
```sql
SELECT t1.uid,
t1.month_d,
count(*) AS total_cnt,
count(t1.submit_time) AS complete_cnt
FROM-- 先求出未完成率的排名
(SELECT UID,
count(submit_time IS NULL OR NULL)/ count(start_time) AS num,
PERCENT_RANK() OVER (
ORDER BY count(submit_time IS NULL OR NULL)/ count(start_time)) AS ranking
FROM exam_record
LEFT JOIN examination_info USING (exam_id)
WHERE tag = 'SQL'
GROUP BY UID) t
INNER JOIN
(-- 再求出近三个月的练习记录
SELECT UID,
date_format(start_time, '%Y%m') AS month_d,
submit_time,
exam_id,
dense_rank() OVER (PARTITION BY UID
ORDER BY date_format(start_time, '%Y%m') DESC) AS ranking
FROM exam_record
LEFT JOIN user_info USING (UID)
WHERE LEVEL IN (6,7) ) t1 USING (UID)
WHERE t1.ranking <= 3 AND t.ranking >= 0.5 -- 使用限制找到符合条件的记录
GROUP BY t1.uid,
t1.month_d
ORDER BY t1.uid,
t1.month_d
```
### 试卷完成数同比 2020 年的增长率及排名变化(困难)
**描述:**
现有试卷信息表 `examination_info``exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间):
| id | exam_id | tag | difficulty | duration | release_time |
| ---- | ------- | ------ | ---------- | -------- | ------------------- |
| 1 | 9001 | SQL | hard | 60 | 2021-01-01 10:00:00 |
| 2 | 9002 | C++ | hard | 80 | 2021-01-01 10:00:00 |
| 3 | 9003 | 算法 | hard | 80 | 2021-01-01 10:00:00 |
| 4 | 9004 | PYTHON | medium | 70 | 2021-01-01 10:00:00 |
试卷作答记录表 `exam_record``uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分):
| id | uid | exam_id | start_time | submit_time | score |
| ---- | ---- | ------- | ------------------- | ------------------- | ----- |
| 1 | 1001 | 9001 | 2020-08-02 10:01:01 | 2020-08-02 10:31:01 | 89 |
| 2 | 1002 | 9001 | 2020-04-01 18:01:01 | 2020-04-01 18:59:02 | 90 |
| 3 | 1001 | 9001 | 2020-04-01 09:01:01 | 2020-04-01 09:21:59 | 80 |
| 5 | 1002 | 9001 | 2021-03-02 19:01:01 | 2021-03-02 19:32:00 | 20 |
| 8 | 1003 | 9001 | 2021-05-02 12:01:01 | 2021-05-02 12:31:01 | 98 |
| 13 | 1003 | 9001 | 2020-01-02 10:01:01 | 2020-01-02 10:31:01 | 89 |
| 9 | 1001 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 |
| 10 | 1002 | 9002 | 2021-02-02 12:01:01 | 2020-02-02 12:43:01 | 81 |
| 11 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 |
| 16 | 1002 | 9002 | 2020-02-02 12:01:01 | | |
| 17 | 1002 | 9002 | 2020-03-02 12:11:01 | | |
| 18 | 1001 | 9002 | 2021-05-05 18:01:01 | | |
| 4 | 1002 | 9003 | 2021-01-20 10:01:01 | 2021-01-20 10:10:01 | 81 |
| 6 | 1001 | 9003 | 2021-04-02 19:01:01 | 2021-04-02 19:40:01 | 89 |
| 15 | 1002 | 9003 | 2021-01-01 18:01:01 | 2021-01-01 18:59:02 | 90 |
| 7 | 1004 | 9004 | 2020-05-02 12:01:01 | 2020-05-02 12:20:01 | 99 |
| 12 | 1001 | 9004 | 2021-09-02 12:11:01 | | |
| 14 | 1002 | 9004 | 2020-01-01 12:11:01 | 2020-01-01 12:31:01 | 83 |
请计算 2021 年上半年各类试卷的做完次数相比 2020 年上半年同期的增长率(百分比格式,保留 1 位小数),以及做完次数排名变化,按增长率和 21 年排名降序输出。
由示例数据结果输出如下:
| tag | exam_cnt_20 | exam_cnt_21 | growth_rate | exam_cnt_rank_20 | exam_cnt_rank_21 | rank_delta |
| ---- | ----------- | ----------- | ----------- | ---------------- | ---------------- | ---------- |
| SQL | 3 | 2 | -33.3% | 1 | 2 | 1 |
解释2020 年上半年有 3 个 tag 有作答完成的记录,分别是 C++、SQL、PYTHON它们被做完的次数分别是 3、3、2做完次数排名为 1、1并列、3
2021 年上半年有 2 个 tag 有作答完成的记录分别是算法、SQL它们被做完的次数分别是 3、2做完次数排名为 1、2具体如下
| tag | start_year | exam_cnt | exam_cnt_rank |
| ------ | ---------- | -------- | ------------- |
| C++ | 2020 | 3 | 1 |
| SQL | 2020 | 3 | 1 |
| PYTHON | 2020 | 2 | 3 |
| 算法 | 2021 | 3 | 1 |
| SQL | 2021 | 2 | 2 |
因此能输出同比结果的 tag 只有 SQL从 2020 到 2021 年,做完次数 3=>2减少 33.3%(保留 1 位小数);排名 1=>2后退 1 名。
**思路:**
本题难点在于长整型的数据类型要求不能有负号产生,用 cast 函数转换数据类型为 signed。
以及用到的`增长率计算公式:(exam_cnt_21-exam_cnt_20)/exam_cnt_20`
做完次数排名变化2021 年和 2020 年比排名升了或者降了多少)
计算公式:`exam_cnt_rank_21 - exam_cnt_rank_20`
在 MySQL 中,`CAST()` 函数用于将一个表达式的数据类型转换为另一个数据类型。它的基本语法如下:
```sql
CAST(expression AS data_type)
-- 将一个字符串转换成整数
SELECT CAST('123' AS INT);
```
示例就不一一举例了,这个函数很简单
**答案:**
```sql
SELECT
tag,
exam_cnt_20,
exam_cnt_21,
concat(
round(
100 * (exam_cnt_21 - exam_cnt_20) / exam_cnt_20,
1
),
'%'
) AS growth_rate,
exam_cnt_rank_20,
exam_cnt_rank_21,
cast(exam_cnt_rank_21 AS signed) - cast(exam_cnt_rank_20 AS signed) AS rank_delta
FROM
(
#2020年、2021年上半年各类试卷的做完次数和做完次数排名
SELECT
tag,
count(
IF (
date_format(start_time, '%Y%m%d') BETWEEN '20200101'
AND '20200630',
start_time,
NULL
)
) AS exam_cnt_20,
count(
IF (
substring(start_time, 1, 10) BETWEEN '2021-01-01'
AND '2021-06-30',
start_time,
NULL
)
) AS exam_cnt_21,
rank() over (
ORDER BY
count(
IF (
date_format(start_time, '%Y%m%d') BETWEEN '20200101'
AND '20200630',
start_time,
NULL
)
) DESC
) AS exam_cnt_rank_20,
rank() over (
ORDER BY
count(
IF (
substring(start_time, 1, 10) BETWEEN '2021-01-01'
AND '2021-06-30',
start_time,
NULL
)
) DESC
) AS exam_cnt_rank_21
FROM
examination_info
JOIN exam_record USING (exam_id)
WHERE
submit_time IS NOT NULL
GROUP BY
tag
) main
WHERE
exam_cnt_21 * exam_cnt_20 <> 0
ORDER BY
growth_rate DESC,
exam_cnt_rank_21 DESC
```
## 聚合窗口函数
### 对试卷得分做 min-max 归一化
**描述:**
现有试卷信息表 `examination_info``exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间):
| id | exam_id | tag | difficulty | duration | release_time |
| ---- | ------- | ------ | ---------- | -------- | ------------------- |
| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 |
| 2 | 9002 | C++ | hard | 80 | 2020-01-01 10:00:00 |
| 3 | 9003 | 算法 | hard | 80 | 2020-01-01 10:00:00 |
| 4 | 9004 | PYTHON | medium | 70 | 2020-01-01 10:00:00 |
试卷作答记录表 `exam_record``uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分):
| id | uid | exam_id | start_time | submit_time | score |
| ---- | ---- | ------- | ------------------- | ------------------- | ------ |
| 6 | 1003 | 9001 | 2020-01-02 12:01:01 | 2020-01-02 12:31:01 | 68 |
| 9 | 1001 | 9001 | 2020-01-02 10:01:01 | 2020-01-02 10:31:01 | 89 |
| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 |
| 12 | 1002 | 9002 | 2021-05-05 18:01:01 | (NULL) | (NULL) |
| 3 | 1004 | 9002 | 2020-01-01 12:01:01 | 2020-01-01 12:11:01 | 60 |
| 2 | 1003 | 9002 | 2020-01-01 19:01:01 | 2020-01-01 19:30:01 | 75 |
| 7 | 1001 | 9002 | 2020-01-02 12:01:01 | 2020-01-02 12:43:01 | 81 |
| 10 | 1002 | 9002 | 2020-01-01 12:11:01 | 2020-01-01 12:31:01 | 83 |
| 4 | 1003 | 9002 | 2020-01-01 12:01:01 | 2020-01-01 12:41:01 | 90 |
| 5 | 1002 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:32:00 | 90 |
| 11 | 1002 | 9004 | 2021-09-06 12:01:01 | (NULL) | (NULL) |
| 8 | 1001 | 9005 | 2020-01-02 12:11:01 | (NULL) | (NULL) |
在物理学及统计学数据计算时,有个概念叫 min-max 标准化,也被称为离差标准化,是对原始数据的线性变换,使结果值映射到[0 - 1]之间。
转换函数为:
![](https://oss.javaguide.cn/github/javaguide/database/sql/29A377601170AB822322431FCDF7EDFE.png)
请你将用户作答高难度试卷的得分在每份试卷作答记录内执行 min-max 归一化后缩放到[0,100]区间,并输出用户 ID、试卷 ID、归一化后分数平均值最后按照试卷 ID 升序、归一化分数降序输出。(注:得分区间默认为[0,100],如果某个试卷作答记录中只有一个得分,那么无需使用公式,归一化并缩放后分数仍为原分数)。
由示例数据结果输出如下:
| uid | exam_id | avg_new_score |
| ---- | ------- | ------------- |
| 1001 | 9001 | 98 |
| 1003 | 9001 | 0 |
| 1002 | 9002 | 88 |
| 1003 | 9002 | 75 |
| 1001 | 9002 | 70 |
| 1004 | 9002 | 0 |
解释:高难度试卷有 9001、9002、9003
作答了 9001 的记录有 3 条,分数分别为 68、89、90按给定公式归一化后分数为0、95、100而后两个得分都是用户 1001 作答的,因此用户 1001 对试卷 9001 的新得分为(95+100)/2≈98只保留整数部分用户 1003 对于试卷 9001 的新得分为 0。最后结果按照试卷 ID 升序、归一化分数降序输出。
**思路:**
注意点:
1. 将高难度的试卷,按每类试卷的得分,利用 max/min (col) over()窗口函数求得各组内最大最小值,然后进行归一化公式计算,缩放区间为[0,100],即 min_max\*100
2. 若某类试卷只有一个得分,则无需使用归一化公式,因只有一个分 max_score=min_score,score公式后结果可能会变成 0。
3. 最后结果按 uid、exam_id 分组求归一化后均值score 为 NULL 的要过滤掉。
最后就是仔细看上面公式 (说实话,这题看起来就很绕)
**答案:**
```sql
SELECT
uid,
exam_id,
round(sum(min_max) / count(score), 0) AS avg_new_score
FROM
(
SELECT
*,
IF (
max_score = min_score,
score,
(score - min_score) / (max_score - min_score) * 100
) AS min_max
FROM
(
SELECT
uid,
a.exam_id,
score,
max(score) over (PARTITION BY a.exam_id) AS max_score,
min(score) over (PARTITION BY a.exam_id) AS min_score
FROM
exam_record a
LEFT JOIN examination_info b USING (exam_id)
WHERE
difficulty = 'hard'
) t
WHERE
score IS NOT NULL
) t1
GROUP BY
uid,
exam_id
ORDER BY
exam_id ASC,
avg_new_score DESC;
```
### 每份试卷每月作答数和截止当月的作答总数
**描述:**
现有试卷作答记录表 exam_recorduid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分):
| id | uid | exam_id | start_time | submit_time | score |
| ---- | ---- | ------- | ------------------- | ------------------- | ------ |
| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 |
| 2 | 1002 | 9001 | 2020-01-20 10:01:01 | 2020-01-20 10:10:01 | 89 |
| 3 | 1002 | 9001 | 2020-02-01 12:11:01 | 2020-02-01 12:31:01 | 83 |
| 4 | 1003 | 9001 | 2020-03-01 19:01:01 | 2020-03-01 19:30:01 | 75 |
| 5 | 1004 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:11:01 | 60 |
| 6 | 1003 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:41:01 | 90 |
| 7 | 1002 | 9001 | 2020-05-02 19:01:01 | 2020-05-02 19:32:00 | 90 |
| 8 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 |
| 9 | 1004 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 |
| 10 | 1003 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:31:01 | 68 |
| 11 | 1001 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:43:01 | 81 |
| 12 | 1001 | 9002 | 2020-03-02 12:11:01 | (NULL) | (NULL) |
请输出每份试卷每月作答数和截止当月的作答总数。
由示例数据结果输出如下:
| exam_id | start_month | month_cnt | cum_exam_cnt |
| ------- | ----------- | --------- | ------------ |
| 9001 | 202001 | 2 | 2 |
| 9001 | 202002 | 1 | 3 |
| 9001 | 202003 | 3 | 6 |
| 9001 | 202005 | 1 | 7 |
| 9002 | 202001 | 1 | 1 |
| 9002 | 202002 | 3 | 4 |
| 9002 | 202003 | 1 | 5 |
解释:试卷 9001 在 202001、202002、202003、202005 共 4 个月有被作答记录,每个月被作答数分别为 2、1、3、1截止当月累积作答总数为 2、3、6、7。
**思路:**
这题就两个关键点:统计截止当月的作答总数、输出每份试卷每月作答数和截止当月的作答总数
这个是关键`**sum(count(*)) over(partition by exam_id order by date_format(start_time,'%Y%m'))**`
**答案:**
```sql
SELECT exam_id,
date_format(start_time, '%Y%m') AS start_month,
count(*) AS month_cnt,
sum(count(*)) OVER (PARTITION BY exam_id
ORDER BY date_format(start_time, '%Y%m')) AS cum_exam_cnt
FROM exam_record
GROUP BY exam_id,
start_month
```
### 每月及截止当月的答题情况(较难)
**描述:**现有试卷作答记录表 `exam_record``uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分):
| id | uid | exam_id | start_time | submit_time | score |
| ---- | ---- | ------- | ------------------- | ------------------- | ------ |
| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 |
| 2 | 1002 | 9001 | 2020-01-20 10:01:01 | 2020-01-20 10:10:01 | 89 |
| 3 | 1002 | 9001 | 2020-02-01 12:11:01 | 2020-02-01 12:31:01 | 83 |
| 4 | 1003 | 9001 | 2020-03-01 19:01:01 | 2020-03-01 19:30:01 | 75 |
| 5 | 1004 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:11:01 | 60 |
| 6 | 1003 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:41:01 | 90 |
| 7 | 1002 | 9001 | 2020-05-02 19:01:01 | 2020-05-02 19:32:00 | 90 |
| 8 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 |
| 9 | 1004 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 |
| 10 | 1003 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:31:01 | 68 |
| 11 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-02-02 12:43:01 | 81 |
| 12 | 1001 | 9002 | 2020-03-02 12:11:01 | (NULL) | (NULL) |
请输出自从有用户作答记录以来,每月的试卷作答记录中月活用户数、新增用户数、截止当月的单月最大新增用户数、截止当月的累积用户数。结果按月份升序输出。
由示例数据结果输出如下:
| start_month | mau | month_add_uv | max_month_add_uv | cum_sum_uv |
| ----------- | ---- | ------------ | ---------------- | ---------- |
| 202001 | 2 | 2 | 2 | 2 |
| 202002 | 4 | 2 | 2 | 4 |
| 202003 | 3 | 0 | 2 | 4 |
| 202005 | 1 | 0 | 2 | 4 |
| month | 1001 | 1002 | 1003 | 1004 |
| ------ | ---- | ---- | ---- | ---- |
| 202001 | 1 | 1 | | |
| 202002 | 1 | 1 | 1 | 1 |
| 202003 | 1 | | 1 | 1 |
| 202005 | | 1 | | |
由上述矩阵可以看出2020 年 1 月有 2 个用户活跃mau=2当月新增用户数为 2
2020 年 2 月有 4 个用户活跃,当月新增用户数为 2最大单月新增用户数为 2当前累积用户数为 4。
**思路:**
难点:
1.如何求每月新增用户
2.截至当月的答题情况
大致流程:
1统计每个人的首次登陆月份 `min()`
2统计每月的月活和新增用户数先得到每个人的首次登陆月份再对首次登陆月份分组求和是该月份的新增人数
3统计截止当月的单月最大新增用户数、截止当月的累积用户数 ,最终按照按月份升序输出
**答案:**
```sql
-- 截止当月的单月最大新增用户数、截止当月的累积用户数,按月份升序输出
SELECT
start_month,
mau,
month_add_uv,
max( month_add_uv ) over ( ORDER BY start_month ),
sum( month_add_uv ) over ( ORDER BY start_month )
FROM
(
-- 统计每月的月活和新增用户数
SELECT
date_format( a.start_time, '%Y%m' ) AS start_month,
count( DISTINCT a.uid ) AS mau,
count( DISTINCT b.uid ) AS month_add_uv
FROM
exam_record a
LEFT JOIN (
-- 统计每个人的首次登陆月份
SELECT uid, min( date_format( start_time, '%Y%m' )) AS first_month FROM exam_record GROUP BY uid ) b ON date_format( a.start_time, '%Y%m' ) = b.first_month
GROUP BY
start_month
) main
ORDER BY
start_month
```

File diff suppressed because it is too large Load Diff