MySQL 常见 SQL 面试题 2
本文档是 《MySQL 常见 SQL 面试题》 的续篇,侧重进阶题型和综合应用:排名/TOP N、窗口函数、自连接、行转列、去重、NULL 处理、UNION、复杂子查询等。每题都给出题目、思路和完整示例 SQL,适合在掌握基础题后继续刷题。
建议:先完成《常见 SQL 面试题》中的基础题,再用本文档练习。示例仍基于 students、courses、scores 三张表(结构见下文「表结构回顾」)。
目录
- 表结构回顾与补充数据
- 排名与 TOP N 题
- 行转列与列转行(透视)
- 去重与重复数据题
- 自连接题
- NULL 处理与条件分支题
- UNION 与多结果集合并
- 复杂子查询与 EXISTS
- 日期与时间范围题
- 综合实战题
- 易错点与书写规范
1. 表结构回顾与补充数据
与第一篇保持一致,三张表如下。若你已有数据,可跳过插入,仅作参考。
-- 学生表
CREATE TABLE IF NOT EXISTS students (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
gender CHAR(1) DEFAULT '男',
age INT,
class_name VARCHAR(20)
);
-- 课程表
CREATE TABLE IF NOT EXISTS courses (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL
);
-- 成绩表
CREATE TABLE IF NOT EXISTS scores (
id INT PRIMARY KEY AUTO_INCREMENT,
student_id INT NOT NULL,
course_id INT NOT NULL,
score DECIMAL(5,2),
exam_date DATE
);
-- 插入示例数据(若已插入可省略)
INSERT INTO students (name, gender, age, class_name) VALUES
('张三', '男', 18, '高一1班'),
('李四', '女', 17, '高一2班'),
('王五', '男', 18, '高一1班'),
('赵六', '女', 17, '高一2班'),
('钱七', '男', 19, '高一1班'),
('孙八', '女', 18, '高一2班');
INSERT INTO courses (name) VALUES ('语文'), ('数学'), ('英语');
INSERT INTO scores (student_id, course_id, score, exam_date) VALUES
(1, 1, 85, '2025-01-10'), (1, 2, 92, '2025-01-11'), (1, 3, 78, '2025-01-12'),
(2, 1, 88, '2025-01-10'), (2, 2, 76, '2025-01-11'), (2, 3, 90, '2025-01-12'),
(3, 1, 72, '2025-01-10'), (3, 2, 85, '2025-01-11'), (3, 3, 88, '2025-01-12'),
(4, 1, 90, '2025-01-10'), (4, 2, 82, '2025-01-11'),
(5, 1, 95, '2025-01-10'), (5, 2, 88, '2025-01-11'),
(6, 1, 80, '2025-01-10'), (6, 2, NULL, '2025-01-11');
2. 排名与 TOP N 题
2.1 查询每门课程成绩最高的学生(姓名、课程名、分数)
思路:先按课程分组得到每门课的最高分,再和成绩表、学生表、课程表关联,匹配“该学生该课程分数 = 该课程最高分”。
SELECT s.name AS 姓名, c.name AS 课程, sc.score AS 分数
FROM scores sc
JOIN students s ON sc.student_id = s.id
JOIN courses c ON sc.course_id = c.id
JOIN (
SELECT course_id, MAX(score) AS max_score
FROM scores
WHERE score IS NOT NULL
GROUP BY course_id
) t ON sc.course_id = t.course_id AND sc.score = t.max_score
ORDER BY c.id;
说明:若同一门课有两人同分且都是最高分,会都显示;若要求只显示一人,可用窗口函数(见下)或再按 id 取一条。
2.2 查询每个学生分数最高的那一门课(姓名、课程名、分数)
思路:先按学生分组得到每人最高分,再和 scores、courses 关联。
SELECT s.name AS 姓名, c.name AS 课程, t.max_score AS 分数
FROM students s
JOIN (
SELECT student_id, MAX(score) AS max_score
FROM scores
WHERE score IS NOT NULL
GROUP BY student_id
) t ON s.id = t.student_id
JOIN scores sc ON sc.student_id = t.student_id AND sc.score = t.max_score
JOIN courses c ON sc.course_id = c.id
ORDER BY s.id;
注意:若同一学生有多门课同分且都是最高,会多行;若要唯一一行可加 LIMIT 1 子查询或窗口函数。
2.3 查询每个班级年龄最大的学生(姓名、班级、年龄)
思路:先按班级求最大年龄,再和学生表匹配。
SELECT s.name AS 姓名, s.class_name AS 班级, s.age AS 年龄
FROM students s
JOIN (
SELECT class_name, MAX(age) AS max_age
FROM students
GROUP BY class_name
) t ON s.class_name = t.class_name AND s.age = t.max_age
ORDER BY s.class_name;
2.4 使用窗口函数:每个学生按总分排名(MySQL 8.0+)
题目:计算每个学生的总分,并按总分降序排名(相同总分并列同一名次,下一名次不连续)。
窗口函数:RANK() OVER (ORDER BY 总分 DESC)。
SELECT name AS 姓名,
total AS 总分,
RANK() OVER (ORDER BY total DESC) AS 排名
FROM (
SELECT s.id, s.name, SUM(sc.score) AS total
FROM students s
JOIN scores sc ON s.id = sc.student_id
GROUP BY s.id, s.name
) t;
若要求“相同分数并列且下一名次连续”,用 DENSE_RANK():
SELECT name AS 姓名,
total AS 总分,
DENSE_RANK() OVER (ORDER BY total DESC) AS 排名
FROM (
SELECT s.id, s.name, SUM(sc.score) AS total
FROM students s
JOIN scores sc ON s.id = sc.student_id
GROUP BY s.id, s.name
) t;
若要求“连续序号、同分也不同名次”,用 ROW_NUMBER():
SELECT name AS 姓名,
total AS 总分,
ROW_NUMBER() OVER (ORDER BY total DESC) AS 序号
FROM (
SELECT s.id, s.name, SUM(sc.score) AS total
FROM students s
JOIN scores sc ON s.id = sc.student_id
GROUP BY s.id, s.name
) t;
2.5 每个班级内按年龄排名(MySQL 8.0+)
题目:在每个班级内部按年龄从大到小排名。
SELECT name AS 姓名,
class_name AS 班级,
age AS 年龄,
RANK() OVER (PARTITION BY class_name ORDER BY age DESC) AS 班内排名
FROM students
ORDER BY class_name, 班内排名;
考点:PARTITION BY class_name 表示“按班级分区”,在各自分区内再 ORDER BY age DESC 排名。
3. 行转列与列转行(透视)
3.1 行转列:每个学生的语文、数学、英语成绩各一列
题目:结果形如:姓名 | 语文 | 数学 | 英语。
写法一:MAX + CASE WHEN(兼容 MySQL 5.x)
SELECT s.name AS 姓名,
MAX(CASE WHEN c.name = '语文' THEN sc.score END) AS 语文,
MAX(CASE WHEN c.name = '数学' THEN sc.score END) AS 数学,
MAX(CASE WHEN c.name = '英语' THEN sc.score END) AS 英语
FROM students s
LEFT JOIN scores sc ON s.id = sc.student_id
LEFT JOIN courses c ON sc.course_id = c.id
GROUP BY s.id, s.name
ORDER BY s.id;
说明:LEFT JOIN 保证没选课的学生也出现,成绩列为 NULL;用 MAX 是因为按学生分组后每门课只有一行,MAX 只是“取唯一值”。
3.2 行转列:并带总分、平均分
在 3.1 基础上加 SUM、AVG:
SELECT s.name AS 姓名,
MAX(CASE WHEN c.name = '语文' THEN sc.score END) AS 语文,
MAX(CASE WHEN c.name = '数学' THEN sc.score END) AS 数学,
MAX(CASE WHEN c.name = '英语' THEN sc.score END) AS 英语,
SUM(sc.score) AS 总分,
AVG(sc.score) AS 平均分
FROM students s
LEFT JOIN scores sc ON s.id = sc.student_id
LEFT JOIN courses c ON sc.course_id = c.id
GROUP BY s.id, s.name
ORDER BY s.id;
4. 去重与重复数据题
4.1 查询有重复成绩的学生(同一学生同一课程考了多次)
假设 scores 表允许同一 (student_id, course_id) 有多条记录(例如多次考试)。要找出“存在重复 (student_id, course_id)”的学生。
SELECT DISTINCT student_id
FROM scores
GROUP BY student_id, course_id
HAVING COUNT(*) > 1;
若要显示学生姓名:
SELECT DISTINCT s.name
FROM students s
JOIN (
SELECT student_id
FROM scores
GROUP BY student_id, course_id
HAVING COUNT(*) > 1
) t ON s.id = t.student_id;
4.2 只保留每个学生每门课最高分的那一条记录(删除重复中分数较低的行)
思路:先找出“要保留”的 (id) 或“要删”的 (id),再 DELETE。若用子查询删除同一张表,MySQL 可能报错,常用 JOIN 写法:
-- 删除“非最高分”的记录(保留每组 student_id, course_id 下 score 最大的一条)
DELETE sc FROM scores sc
JOIN (
SELECT student_id, course_id, MAX(score) AS max_score
FROM scores
GROUP BY student_id, course_id
HAVING COUNT(*) > 1
) t ON sc.student_id = t.student_id AND sc.course_id = t.course_id AND sc.score < t.max_score;
注意:实际表若无重复可先备份或跳过执行;这里仅作语法与思路示例。
5. 自连接题
5.1 查询同班同学配对(每对只显示一次,如 A-B 不再显示 B-A)
题目:每个学生列出其同班另一名同学,且 (A, B) 与 (B, A) 只保留一组(如按 id 小在前)。
SELECT s1.name AS 学生A, s2.name AS 学生B
FROM students s1
JOIN students s2 ON s1.class_name = s2.class_name AND s1.id < s2.id
ORDER BY s1.id, s2.id;
说明:s1.id < s2.id 保证配对不重复且自己不和自己配对。
5.2 查询年龄相同的学生对(姓名、年龄,且 id 小在前)
SELECT s1.name AS 学生A, s2.name AS 学生B, s1.age AS 年龄
FROM students s1
JOIN students s2 ON s1.age = s2.age AND s1.id < s2.id
ORDER BY s1.age, s1.id;
6. NULL 处理与条件分支题
6.1 成绩列:NULL 显示为 0,并计算总分
SELECT s.name,
IFNULL(sc.score, 0) AS 分数,
SUM(IFNULL(sc.score, 0)) OVER (PARTITION BY s.id) AS 总分
FROM students s
LEFT JOIN scores sc ON s.id = sc.student_id;
若不用窗口函数,可先分组再 JOIN:
SELECT s.name,
sc.course_id,
IFNULL(sc.score, 0) AS 分数
FROM students s
LEFT JOIN scores sc ON s.id = sc.student_id;
-- 总分需另写一查询按 student_id SUM(IFNULL(score,0))
6.2 按分数段打等级:优秀(≥90)、良好(80~89)、及格(60~79)、不及格(<60)
SELECT s.name AS 姓名,
c.name AS 课程,
sc.score AS 分数,
CASE
WHEN sc.score IS NULL THEN '缺考'
WHEN sc.score >= 90 THEN '优秀'
WHEN sc.score >= 80 THEN '良好'
WHEN sc.score >= 60 THEN '及格'
ELSE '不及格'
END AS 等级
FROM students s
JOIN scores sc ON s.id = sc.student_id
JOIN courses c ON sc.course_id = c.id
ORDER BY s.id, c.id;
6.3 统计各等级人数(优秀/良好/及格/不及格各多少人)
SELECT
CASE
WHEN score IS NULL THEN '缺考'
WHEN score >= 90 THEN '优秀'
WHEN score >= 80 THEN '良好'
WHEN score >= 60 THEN '及格'
ELSE '不及格'
END AS 等级,
COUNT(*) AS 人数
FROM scores
GROUP BY
CASE
WHEN score IS NULL THEN '缺考'
WHEN score >= 90 THEN '优秀'
WHEN score >= 80 THEN '良好'
WHEN score >= 60 THEN '及格'
ELSE '不及格'
END;
MySQL 中可对“等级”用数字或字母编码再 GROUP BY,避免重复写 CASE;这里为可读性直接按 CASE 分组。
7. UNION 与多结果集合并
7.1 查询“语文成绩高于 85 或数学成绩高于 85”的学生(去重)
题目:同一学生只要满足其一即出现一次。
SELECT student_id FROM scores sc JOIN courses c ON sc.course_id = c.id WHERE c.name = '语文' AND sc.score > 85
UNION
SELECT student_id FROM scores sc JOIN courses c ON sc.course_id = c.id WHERE c.name = '数学' AND sc.score > 85;
UNION 会去重;若需要保留重复(同一学生满足两次也显示两次)用 UNION ALL。
7.2 分别统计“男生人数”和“女生人数”,结果两行(性别、人数)
SELECT '男' AS 性别, COUNT(*) AS 人数 FROM students WHERE gender = '男'
UNION ALL
SELECT '女', COUNT(*) FROM students WHERE gender = '女';
8. 复杂子查询与 EXISTS
8.1 查询“至少选修了学号为 1 的学生所选修的全部课程”的学生(除自己外)
题目:即:不存在“学号 1 选了但该学生没选”的课程。
思路:对每个学生 s,用 NOT EXISTS:不存在这样一门课 c,学号 1 选了而 s 没选。
SELECT DISTINCT s.id, s.name
FROM students s
JOIN scores sc ON s.id = sc.student_id
WHERE s.id != 1
AND NOT EXISTS (
SELECT 1
FROM scores s1
WHERE s1.student_id = 1
AND NOT EXISTS (
SELECT 1
FROM scores s2
WHERE s2.student_id = s.id AND s2.course_id = s1.course_id
)
);
说明:内层 NOT EXISTS 表示“学号 1 选的某门课,当前学生 s 没选”;外层 NOT EXISTS 表示“不存在这样的课”,即 s 选了学号 1 选的所有课。
8.2 查询“语文成绩高于数学成绩”的学生(姓名、语文分、数学分)
思路:每个学生取语文一行、数学一行,再比较。可用自连接或子查询。
SELECT s.name AS 姓名,
a.score AS 语文,
b.score AS 数学
FROM students s
JOIN scores a ON s.id = a.student_id
JOIN courses c1 ON a.course_id = c1.id AND c1.name = '语文'
JOIN scores b ON s.id = b.student_id
JOIN courses c2 ON b.course_id = c2.id AND c2.name = '数学'
WHERE a.score > b.score;
9. 日期与时间范围题
9.1 查询某次考试日期(如 2025-01-10)各科平均分
SELECT c.name AS 课程, AVG(sc.score) AS 平均分
FROM scores sc
JOIN courses c ON sc.course_id = c.id
WHERE sc.exam_date = '2025-01-10'
GROUP BY c.id, c.name;
9.2 查询最近一次考试(按 exam_date 最大)的每个学生总分
SELECT s.name, SUM(sc.score) AS 总分
FROM students s
JOIN scores sc ON s.id = sc.student_id
WHERE sc.exam_date = (SELECT MAX(exam_date) FROM scores)
GROUP BY s.id, s.name;
9.3 查询“连续两天都有考试记录”的日期对(示例)
若 scores 表有多次考试日期,可先列出日期,再自连接找“相差一天”的日期对(此处仅作思路示例,表内需有多个不同 exam_date):
SELECT DISTINCT a.exam_date AS 日期1, b.exam_date AS 日期2
FROM (SELECT DISTINCT exam_date FROM scores) a
JOIN (SELECT DISTINCT exam_date FROM scores) b
ON DATEDIFF(b.exam_date, a.exam_date) = 1;
10. 综合实战题
10.1 每个班级总分第一的学生(姓名、班级、总分)
思路:先按班级求每人总分,再按班级分组取每组最高总分,再关联回学生和总分。
SELECT t.class_name AS 班级, s.name AS 姓名, t.total AS 总分
FROM (
SELECT s.id, s.name, s.class_name, SUM(sc.score) AS total
FROM students s
JOIN scores sc ON s.id = sc.student_id
GROUP BY s.id, s.name, s.class_name
) t
JOIN (
SELECT class_name, MAX(total) AS max_total
FROM (
SELECT s.class_name, SUM(sc.score) AS total
FROM students s
JOIN scores sc ON s.id = sc.student_id
GROUP BY s.id, s.class_name
) x
GROUP BY class_name
) m ON t.class_name = m.class_name AND t.total = m.max_total
JOIN students s ON t.id = s.id
ORDER BY t.class_name;
简化写法(先建“每人总分”再按班取最大):
WITH stu_total AS (
SELECT s.id, s.name, s.class_name, SUM(sc.score) AS total
FROM students s
JOIN scores sc ON s.id = sc.student_id
GROUP BY s.id, s.name, s.class_name
)
SELECT st.name AS 姓名, st.class_name AS 班级, st.total AS 总分
FROM stu_total st
JOIN (
SELECT class_name, MAX(total) AS max_total
FROM stu_total
GROUP BY class_name
) t ON st.class_name = t.class_name AND st.total = t.max_total
ORDER BY st.class_name;
(MySQL 8.0+ 支持 WITH;5.7 可用内联子查询代替 WITH。)
10.2 查询“至少有一门课成绩高于该科平均分”的学生
思路:对每个学生每条成绩,判断该科平均分;若存在一门 score > 该科平均分即可。
SELECT DISTINCT s.id, s.name
FROM students s
JOIN scores sc ON s.id = sc.student_id
JOIN (
SELECT course_id, AVG(score) AS avg_score
FROM scores
WHERE score IS NOT NULL
GROUP BY course_id
) t ON sc.course_id = t.course_id
WHERE sc.score > t.avg_score;
10.3 成绩表:每门课比上一条记录(按 id)高多少分(同一学生)
题目:按 student_id、course_id 分组,按 id 排序,当前行分数减去“上一行”分数(MySQL 8.0 用 LAG)。
SELECT student_id,
course_id,
score,
LAG(score) OVER (PARTITION BY student_id, course_id ORDER BY id) AS 上一行分数,
score - LAG(score) OVER (PARTITION BY student_id, course_id ORDER BY id) AS 分数差
FROM scores;
11. 易错点与书写规范
- UPDATE/DELETE 务必带 WHERE,避免误改/误删全表。
- JOIN 必须写 ON,否则是笛卡尔积。
- GROUP BY 时,SELECT 中非聚合列尽量都出现在 GROUP BY 中(严格模式要求)。
- NULL 判断用 IS NULL / IS NOT NULL,不用
= NULL。 - 子查询删除/更新同一张表时,用 JOIN 或派生表避免“同一表同时读写的限制”。
- UNION 去重、UNION ALL 不去重;两边的列数、类型要对应。
- 窗口函数 OVER (PARTITION BY … ORDER BY …) 中,ORDER BY 决定窗口内顺序;是否有 PARTITION BY 决定“全局”还是“分区内”计算。
建议在本地建好 students、courses、scores 后,按章节逐题敲一遍 SQL 并执行,再尝试自己变形(如改成分数、班级、日期条件)。配合《常见 SQL 面试题》一起使用,可覆盖大部分初中级 SQL 笔试与面试题。