SQL 手撕题库——50 道高频题
每道题统一按 4 步讲:① 题目本质 ② 解题关键 ③ 核心 SQL ④ 可能的坑。 题号沿用原题库编号(部分题号在原题库中已空缺),标 ⭐ 的为重点题。
预备题:班级成绩三连问
A. 查询每个班的平均分和班级 id
① 题目本质:分组聚合问题
② 解题关键:按 class_id 分组,用 AVG(score)
③ 核心 SQL
SELECT class_id, AVG(score) AS avg_score
FROM student
GROUP BY class_id;
class_id score student
select class_id, AVG(score) as avg_score
from student
group by class_id;
④ 可能的坑
- 非聚合字段必须出现在
GROUP BY NULL分数会被AVG自动忽略- 数据量大时
GROUP BY可能走临时表
B. 查询每个班分数最高的人的 id 和分数
① 题目本质:分组取最大值
② 解题关键:GROUP BY class_id + MAX(score),再关联原表取 id
③ 核心 SQL
SELECT s.student_id, s.class_id, s.score
FROM student s
INNER JOIN (SELECT class_id, MAX(score) max_score
FROM student
GROUP BY class_id) t
ON s.class_id = t.class_id AND s.score = t.max_score;
SELECT s.student_id, s.class_id, s.score
FROM student s
INNER JOIN (SELECT class_id, MAX(score) max_score
FROM student
GROUP BY class_id) t
WHERE s.class_id = t.class_id AND s.score = t.max_score;
④ 可能的坑
- 同分并列时会返回多行,需确认业务是否允许
- 不能直接
SELECT student_id, class_id, MAX(score)混用非聚合字段
C. 查询每个班级前五名
① 题目本质:分组内 TopN 排名
② 解题关键:窗口函数 ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC)
③ 核心 SQL
SELECT *
FROM (SELECT student_id,
class_id,
score,
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC) rk
FROM student) t
WHERE rk <= 5;
SELECT * FROM
(SELECT student_id, class_id, score, ROW_NUMBER() OVER(PATITION BY class_id ORDER BY score DESC) rk FROM student)
WHERE rk <= 5;
④ 可能的坑
ROW_NUMBER不处理并列,RANK并列会跳号,DENSE_RANK并列不跳号——看业务选- 窗口函数别名在
WHERE中不可见,必须套一层子查询
第 1 题:01 课程比 02 课程成绩高的学生(重点)⭐
① 题目本质:同一学生两门课成绩的一对一比较
② 解题关键:把 01 和 02 成绩分别派生为两张表,再 INNER JOIN 关联同一个 s_id 比较
③ 核心 SQL
SELECT s.s_id, s.s_name, s1.s_score01 `01`, s2.s_score02 `02`
FROM (SELECT s_id, s_score s_score01 FROM Score WHERE c_id = '01') s1
INNER JOIN (SELECT s_id, s_score s_score02 FROM Score WHERE c_id = '02') s2
ON s1.s_id = s2.s_id
INNER JOIN Student s ON s.s_id = s1.s_id
WHERE s1.s_score01 > s2.s_score02;
也可用
CASE WHEN行转列 +HAVING比较(更简洁):
SELECT s_id, s_name,
MAX(CASE WHEN c_id = '01' THEN s_score END) `01`,
MAX(CASE WHEN c_id = '02' THEN s_score END) `02`
FROM Score
INNER JOIN Student USING (s_id)
GROUP BY s_id, s_name
HAVING `01` > `02`;
④ 可能的坑
- 只选了 01 没选 02(或反之)的学生,
INNER JOIN会自动排除——通常符合题意 HAVING里引用别名要用反引号(纯数字开头的标识符)AS别名只能在SELECT里起,HAVING只能用别名不能起新别名
第 2 题:平均成绩大于 60 分的学生(重点)⭐
① 题目本质:聚合过滤
② 解题关键:GROUP BY s_id + AVG 聚合 + HAVING 过滤
③ 核心 SQL
SELECT s.s_id, s.s_name, FORMAT(AVG(sc.s_score), 2) avg_score
FROM Student s
INNER JOIN Score sc ON s.s_id = sc.s_id
GROUP BY s.s_id, s.s_name
HAVING AVG(sc.s_score) > 60;
④ 可能的坑
- 聚合条件必须用
HAVING,不能用WHERE FORMAT返回字符串,若需数值排序应改用ROUND
第 3 题:所有学生的学号、姓名、选课数、总成绩(不重要)
① 题目本质:含 NULL 的聚合展示
② 解题关键:LEFT JOIN 保留没选课的学生;COUNT(c_id) 忽略 NULL;COALESCE 处理总分 NULL
③ 核心 SQL
SELECT s.s_id, s.s_name,
COUNT(sc.c_id) course_count,
COALESCE(SUM(sc.s_score), 0) total_score
FROM Student s
LEFT JOIN Score sc ON s.s_id = sc.s_id
GROUP BY s.s_id, s.s_name;
④ 可能的坑
INNER JOIN会丢失没有选课的学生,必须用LEFT JOINCOUNT(*)会把左连接补出的 NULL 行也算进去,应用COUNT(c_id)- 不要用
IFNULL,用COALESCE(标准 SQL,可移植)
第 4 题:姓”猴”的老师个数(不重要)
① 题目本质:模糊匹配 + 计数
② 解题关键:LIKE '猴%' + COUNT(*)
③ 核心 SQL
SELECT COUNT(*) number
FROM Teacher
WHERE t_name LIKE '猴%';
④ 可能的坑
%猴%是包含,猴%是姓猴,区分清楚- 大小写敏感取决于字符集排序规则
第 5 题:没学过”张三”老师课的学生(重点)⭐
① 题目本质:“没有”→ 反向过滤,典型 NOT EXISTS 场景
② 解题关键:先找”学过张三课的学生集合”,再取补集
③ 核心 SQL
-- NOT EXISTS 写法(推荐)
SELECT s.s_id, s.s_name
FROM Student s
WHERE NOT EXISTS (
SELECT 1
FROM Score sc
INNER JOIN Course c ON sc.c_id = c.c_id
INNER JOIN Teacher t ON c.t_id = t.t_id
WHERE sc.s_id = s.s_id
AND t.t_name = '张三'
);
SELECT s.s_id, s.s_name
FROM student s
WHERE NOT EXISTS(
SELECT 1
FROM score sc
INNER JOIN Course c on sc.c_id = c.c_id
INNer JOIN Teacher t on c.t_id = t.t_id
where sc.s_id = s.s_id AND t.t_name = '张三');
④ 可能的坑
NOT IN遇到子查询结果含 NULL 会导致全部过滤——优先用NOT EXISTSLEFT JOIN + WHERE ts.s_id IS NULL(反连接)也可以,但语义不如NOT EXISTS直观
想一想:为什么
NOT IN一旦子查询结果里有 NULL,就整表变空?因为
x NOT IN (1, NULL)会被展开成x != 1 AND x != NULL。任何值与NULL比较的结果都是NULL(未知),未知 AND 真仍是未知,WHERE只保留结果为真的行,于是没有任何行能通过。NOT EXISTS判断的是「是否存在这样一行」,不存在就是明确的不存在,不会被 NULL 干扰,所以更稳。
第 7 题:同时学过 01 和 02 课程的学生(重点)⭐
① 题目本质:集合交——一个学生必须同时满足两个条件
② 解题关键:对同一个 s_id 的课程做 GROUP BY + HAVING COUNT = 2
③ 核心 SQL
SELECT s.s_id, s.s_name
FROM Student s
INNER JOIN (SELECT s_id
FROM Score
WHERE c_id IN ('01', '02')
GROUP BY s_id
HAVING COUNT(DISTINCT c_id) = 2) scc
ON s.s_id = scc.s_id;
④ 可能的坑
HAVING COUNT(s_name) = 2只是侥幸对,应用COUNT(DISTINCT c_id) = 2LEFT JOIN条件写进WHERE会退化为INNER JOIN
想一想:为什么这里必须用
COUNT(DISTINCT c_id) = 2,而不是COUNT(*) = 2或COUNT(s_name) = 2?因为同一学生可能对同一门课有多条成绩记录(补考、重修等),
COUNT(*)数的是行数,两行可能都是 01 课,就会误判成「学了两门」。COUNT(DISTINCT c_id)数的是不同课程的个数,才真正表达了「同时学过 01 和 02」这个集合语义。
第 8 题:02 课程的总成绩(不重点)
① 题目本质:简单聚合
② 解题关键:WHERE c_id = '02' + SUM
③ 核心 SQL
SELECT SUM(s_score) total
FROM Score
WHERE c_id = '02';
④ 可能的坑
SUM对 NULL 安全(忽略),但若全为 NULL 返回 NULL,可用COALESCE(SUM(...), 0)兜底
第 9A 题:有课程成绩小于 60 的学生(简单)
① 题目本质:行过滤,只要存在一门 < 60 即入选
② 解题关键:INNER JOIN + WHERE sc.s_score < 60 + DISTINCT 去重
③ 核心 SQL
SELECT DISTINCT s.s_id, s.s_name
FROM Student s
INNER JOIN Score sc ON s.s_id = sc.s_id
WHERE sc.s_score < 60;
④ 可能的坑
- 一个学生可能多门不及格,必须加
DISTINCT
第 9B 题:所有课程成绩都小于 60 的学生(难)
① 题目本质:全量满足条件——每门课都 < 60
② 解题关键:HAVING SUM(s_score >= 60) = 0(布尔转 0/1)或 NOT EXISTS 反向
③ 核心 SQL
-- HAVING 写法
SELECT s.s_id, s.s_name
FROM Student s
INNER JOIN Score sc ON s.s_id = sc.s_id
GROUP BY s.s_id, s.s_name
HAVING SUM(sc.s_score >= 60) = 0;
-- NOT EXISTS 写法
SELECT s.s_id, s.s_name
FROM Student s
WHERE NOT EXISTS (
SELECT 1
FROM Score sc
WHERE sc.s_id = s.s_id
AND sc.s_score >= 60
);
④ 可能的坑
sc.s_score < 60是布尔表达式,在 MySQL 中转为 0/1,SUM可以直接用- 注意与 9A(有一门 < 60)语义完全不同
想一想:「有一门 < 60」和「所有课程都 < 60」差在哪?为什么后者更难?
「有一门 < 60」是存在性判断,一行
WHERE就能表达;「所有课程都 < 60」是全称判断,等价于「不存在任何一门 ≥ 60 的课」。所以要把全称命题翻译成「没有反例」——用NOT EXISTS或HAVING SUM(score >= 60) = 0来表达。
第 10 题:没有学全所有课的学生(重点)⭐
① 题目本质:学生选课数 < 总课程数
② 解题关键:子查询统计该学生选课数,与 Course 总数比较
③ 核心 SQL
SELECT s.s_id, s.s_name
FROM Student s
WHERE s.s_id NOT IN (
SELECT sc.s_id
FROM Score sc
GROUP BY sc.s_id
HAVING COUNT(sc.c_id) = (SELECT COUNT(*) FROM Course)
);
④ 可能的坑
NOT IN子查询含 NULL 会全部过滤,此处sc.s_id不为 NULL 所以安全COUNT(sc.c_id)与COUNT(*)在有 NULL 时有区别
第 11 题:至少有一门课与 01 同学相同的学生(重点)⭐
① 题目本质:集合相交——课程集合有交集
② 解题关键:用 IN 子查询找出 01 同学选的课,再找选过其中任意一门的学生
③ 核心 SQL
SELECT DISTINCT s.s_id, s.s_name
FROM Student s
INNER JOIN Score sc ON s.s_id = sc.s_id
WHERE sc.c_id IN (SELECT c_id FROM Score WHERE s_id = '01')
AND s.s_id != '01';
④ 可能的坑
- 要排除 01 同学本人(
AND s.s_id != '01') IN子查询不含 NULL 时安全;含 NULL 时改用EXISTS
第 12 题:与 01 同学所学课程完全相同的其他同学(重点)⭐
① 题目本质:集合相等——双向包含
② 解题关键:
- 没有 01 没学的课(
NOT EXISTS排除) - 且选课数相同(
COUNT相等)
③ 核心 SQL
-- GROUP_CONCAT 字符串比较法(简洁)
SELECT scc.s_id
FROM (SELECT s_id, GROUP_CONCAT(c_id ORDER BY c_id SEPARATOR ',') course
FROM Score
GROUP BY s_id) scc
WHERE scc.course = (SELECT GROUP_CONCAT(c_id ORDER BY c_id SEPARATOR ',')
FROM Score
WHERE s_id = '01'
GROUP BY s_id)
AND scc.s_id != '01';
-- COUNT 法(稳健)
SELECT scc.s_id
FROM Score scc
WHERE scc.c_id IN (SELECT c_id FROM Score WHERE s_id = '01')
AND scc.s_id != '01'
GROUP BY scc.s_id
HAVING COUNT(DISTINCT scc.c_id) = (SELECT COUNT(DISTINCT c_id) FROM Score WHERE s_id = '01');
④ 可能的坑
- 光检查”选的课都在 01 的课里”不够,还要保证数量相等
DISTINCT在子查询中会改变集合粒度,影响NOT IN逻辑结果——推荐用NOT EXISTSGROUP_CONCAT默认长度 1024,超长会截断
想一想:为什么只检查「选的课都在 01 的课里」还不够,还必须
COUNT相等?因为「都在 01 的课里」只保证了子集关系(没选 01 之外的课),没保证数量:一个只选了 01 这一门课的学生也满足子集条件,却不是「完全相同」。子集 + 数量相等,才能推出集合相等。
第 15 题:两门及以上不及格的学生(重点)⭐
① 题目本质:分组聚合 + 条件筛选
② 解题关键:HAVING COUNT(s_score < 60 的行) >= 2,利用布尔表达式转 0/1
③ 核心 SQL
SELECT s.s_id, s.s_name, FORMAT(AVG(sc.s_score), 2) avg_score
FROM Student s
INNER JOIN Score sc ON s.s_id = sc.s_id
GROUP BY s.s_id, s.s_name
HAVING SUM(sc.s_score < 60) >= 2;
④ 可能的坑
SUM(sc.s_score < 60)是 MySQL 特有布尔转数值技巧,标准写法用SUM(CASE WHEN ... THEN 1 ELSE 0 END)- 平均成绩是全部课程的均值,不只是不及格科目的均值
第 16 题:01 课程分数 < 60 按分数降序(不重点)
① 题目本质:简单过滤 + 排序
② 解题关键:WHERE c_id = '01' AND s_score < 60 + ORDER BY DESC
③ 核心 SQL
SELECT s.s_id, s.s_name, scc.s_score
FROM Student s
INNER JOIN (SELECT s_id, s_score
FROM Score
WHERE s_score < 60
AND c_id = '01') scc
ON s.s_id = scc.s_id
ORDER BY scc.s_score DESC;
④ 可能的坑
ORDER BY排在最外层,不能在子查询里悬空使用
第 17 题:按平均成绩展示所有课程成绩(重重点 = 35 题)⭐⭐
① 题目本质:行转列 + 排序
② 解题关键:CASE WHEN c_id = 'XX' THEN s_score END 行转列 + MAX 聚合去重 + 派生表提供均值
③ 核心 SQL
SELECT sc.s_id `学号`,
MAX(CASE WHEN c_id = '01' THEN s_score END) `语文`,
MAX(CASE WHEN c_id = '02' THEN s_score END) `数学`,
MAX(CASE WHEN c_id = '03' THEN s_score END) `英语`,
scc.avg `平均成绩`
FROM Score sc
INNER JOIN (SELECT s_id, FORMAT(AVG(s_score), 2) avg
FROM Score
GROUP BY s_id) scc ON sc.s_id = scc.s_id
GROUP BY sc.s_id
ORDER BY scc.avg DESC;
④ 可能的坑
- 不加
MAX则GROUP BY后CASE WHEN每行只取一门,出现大量 NULL FORMAT返回字符串,ORDER BY排序会变字符串比较,需确认是否影响结果
第 18 题:各科最高最低平均分及各段率(综合重点)⭐
① 题目本质:多维聚合展示
② 解题关键:CASE WHEN 配合 SUM / COUNT 计算比率;标准写法比 AVG(布尔) 更通用
③ 核心 SQL
SELECT sc.c_id `课程ID`,
c.c_name `课程名`,
MAX(sc.s_score) `最高分`,
MIN(sc.s_score) `最低分`,
FORMAT(AVG(sc.s_score), 2) `平均分`,
FORMAT(SUM(CASE WHEN sc.s_score >= 60 THEN 1 ELSE 0 END) / COUNT(sc.s_id), 2) `及格率`,
FORMAT(SUM(CASE WHEN sc.s_score >= 70 AND sc.s_score < 80 THEN 1 ELSE 0 END) / COUNT(sc.s_id), 2) `中等率`,
FORMAT(SUM(CASE WHEN sc.s_score >= 80 AND sc.s_score < 90 THEN 1 ELSE 0 END) / COUNT(sc.s_id), 2) `优良率`,
FORMAT(SUM(CASE WHEN sc.s_score >= 90 THEN 1 ELSE 0 END) / COUNT(sc.s_id), 2) `优秀率`
FROM Score sc
INNER JOIN Course c ON sc.c_id = c.c_id
GROUP BY sc.c_id, c.c_name;
④ 可能的坑
AVG(sc.s_score >= 60)是 MySQL 语法糖,面试写CASE WHEN更工程化- 分组必须同时包含
c_id和c_name
第 19 题:各科成绩排名(重点窗口函数)⭐
① 题目本质:分组内排名
② 解题关键:ROW_NUMBER() OVER (PARTITION BY c_id ORDER BY s_score DESC)
③ 核心 SQL
SELECT s_id, c_id,
ROW_NUMBER() OVER (PARTITION BY c_id ORDER BY s_score DESC) `rank`,
s_score
FROM Score;
三种窗口函数区别:
ROW_NUMBER:1 2 3 4(无并列)DENSE_RANK:1 2 2 3(并列不跳号)RANK:1 2 2 4(并列跳号)
④ 可能的坑
- 窗口函数别名在
WHERE不可见,取指定名次需套子查询 - 忘记
DESC会变成正序(分数低的排第一)
想一想:
ROW_NUMBER、RANK、DENSE_RANK在并列时行为有何不同,什么时候该用后两者?
ROW_NUMBER永远连续编号 1 2 3 4,并列会被强行拆开;RANK并列同号、下一名跳号(1 2 2 4);DENSE_RANK并列同号且不跳号(1 2 2 3)。业务要「并列算同一名次」时用RANK或DENSE_RANK,要「严格取固定人数」时用ROW_NUMBER。
第 20 题:学生总成绩排名(不重点)
① 题目本质:全局排名(不分组)
② 解题关键:先 GROUP BY 算总分,再用 ROW_NUMBER() OVER (ORDER BY 总分 DESC)
③ 核心 SQL
SELECT ROW_NUMBER() OVER (ORDER BY SUM(s_score) DESC) `rank`,
s_id,
SUM(s_score) total
FROM Score
GROUP BY s_id;
④ 可能的坑
- 全局排名不需要
PARTITION BY,加了PARTITION BY s_id每人都是第一名
第 21 题:不同老师所教课程平均分排序(不重点)
① 题目本质:多表 JOIN + 分组 + 排序
② 解题关键:联表拿到老师名和课程名后 GROUP BY 两字段
③ 核心 SQL
SELECT t.t_name, c.c_name, FORMAT(AVG(sc.s_score), 2) avg_score
FROM Score sc
INNER JOIN Course c ON sc.c_id = c.c_id
INNER JOIN Teacher t ON c.t_id = t.t_id
GROUP BY t.t_name, c.c_name
ORDER BY avg_score DESC;
④ 可能的坑
ORDER BY引用SELECT别名在 MySQL 中合法,但其他数据库需注意
第 22 题:各课程成绩第 2 ~ 3 名的学生(重要)⭐
① 题目本质:分组内取指定名次区间
② 解题关键:窗口函数分组排名后,外层 WHERE rank BETWEEN 2 AND 3
③ 核心 SQL
SELECT scc.c_id, s.s_id, s.s_name, scc.`rank`, scc.s_score
FROM Student s
INNER JOIN (SELECT *,
ROW_NUMBER() OVER (PARTITION BY c_id ORDER BY s_score DESC) `rank`
FROM Score) scc
ON s.s_id = scc.s_id
WHERE scc.`rank` BETWEEN 2 AND 3;
④ 可能的坑
BETWEEN 2 AND 3是左闭右闭,包含 2 和 3- 窗口函数别名不能直接用在
WHERE里,必须包一层
第 23 题:各科分段人数统计(重点)⭐
① 题目本质:分段统计,行转列
② 解题关键:CASE WHEN 区间判断 + SUM 聚合
③ 核心 SQL
SELECT c.c_id `课程ID`,
c.c_name `课程名称`,
SUM(CASE WHEN sc.s_score >= 85 THEN 1 ELSE 0 END) `[100-85]`,
SUM(CASE WHEN sc.s_score >= 70 AND sc.s_score < 85 THEN 1 ELSE 0 END) `[85-70]`,
SUM(CASE WHEN sc.s_score >= 60 AND sc.s_score < 70 THEN 1 ELSE 0 END) `[70-60]`,
SUM(CASE WHEN sc.s_score < 60 THEN 1 ELSE 0 END) `[<60]`
FROM Course c
INNER JOIN Score sc ON c.c_id = sc.c_id
GROUP BY c.c_id, c.c_name;
④ 可能的坑
- 区间边界不能重叠,注意
>=和<的配合 - 遇到分区间问题:直接上
CASE WHEN
第 24 题:学生平均成绩及名次(重点)⭐
① 题目本质:全局排名
② 解题关键:GROUP BY 算均值后,ROW_NUMBER() OVER (ORDER BY AVG DESC)
③ 核心 SQL
SELECT s_id,
FORMAT(AVG(s_score), 2) avg_score,
ROW_NUMBER() OVER (ORDER BY AVG(s_score) DESC) `rank`
FROM Score
GROUP BY s_id;
④ 可能的坑
- 全局排名不加
PARTITION BY
第 25 题:各科成绩前三名(重点,同 22 题)⭐
① 题目本质:分组内 Top3
② 解题关键:ROW_NUMBER() OVER (PARTITION BY c_id ORDER BY s_score DESC),外层 WHERE rank <= 3
③ 核心 SQL
SELECT *
FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY c_id ORDER BY s_score DESC) rk
FROM Score) t
WHERE rk <= 3;
④ 可能的坑:同第 22 题
第 26 题:每门课程被选修的学生数(不重点)
① 题目本质:分组计数
② 解题关键:GROUP BY c_id + COUNT
③ 核心 SQL
SELECT c.c_name, COUNT(sc.c_id) number
FROM Score sc
INNER JOIN Course c ON sc.c_id = c.c_id
GROUP BY c.c_name;
④ 可能的坑:JOIN 记得写 ON
第 27 题:只选了两门课的学生(不重点)
① 题目本质:分组计数 = 2
② 解题关键:GROUP BY + HAVING COUNT(DISTINCT c_id) = 2
③ 核心 SQL
SELECT s.s_id, s.s_name
FROM Student s
INNER JOIN Score sc ON s.s_id = sc.s_id
GROUP BY s.s_id, s.s_name
HAVING COUNT(DISTINCT c_id) = 2;
④ 可能的坑:能用 INNER JOIN ON 就不用 IN,IN 有时不走索引
第 28 题:男女生人数(不重点)
① 题目本质:条件计数
② 解题关键:SUM(CASE WHEN sex = '男' THEN 1 ELSE 0 END)
③ 核心 SQL
SELECT SUM(CASE WHEN s_sex = '男' THEN 1 ELSE 0 END) `男`,
SUM(CASE WHEN s_sex = '女' THEN 1 ELSE 0 END) `女`
FROM Student;
④ 可能的坑:简单场景也可用 IF(condition, 1, 0),但面试写 CASE WHEN 更稳
第 29 题:姓名含”风”的学生(不重点)
① 题目本质:模糊匹配
② 解题关键:LIKE '%风%'
③ 核心 SQL
SELECT * FROM Student WHERE s_name LIKE '%风%';
④ 可能的坑:% 在两端会导致全表扫描,无法用索引
第 31 题:1990 年出生的学生(重点,日期函数)⭐
① 题目本质:日期字段过滤
② 解题关键:YEAR(s_birth) = 1990;如果字段是 VARCHAR 不能依赖 BETWEEN
③ 核心 SQL
SELECT * FROM Student WHERE YEAR(s_birth) = 1990;
常用时间函数(加分项):
SELECT DATE_FORMAT(NOW(), '%W %M %Y'),
DATE_ADD(NOW(), INTERVAL 7 DAY),
DATEDIFF(NOW(), '2025-01-13');
④ 可能的坑
s_birth是 VARCHAR 时,BETWEEN '1990-01-01' AND '1990-12-31'是字符串比较,需格式严格为YYYY-MM-DDYEAR()函数走不了索引,数据量大时考虑改为范围查询
第 32 题:平均成绩 ≥ 85 的学生(不重要)
① 题目本质:聚合过滤
② 解题关键:HAVING AVG(s_score) >= 85
③ 核心 SQL
SELECT s.s_id, s.s_name, FORMAT(AVG(sc.s_score), 2) avg_score
FROM Student s
INNER JOIN Score sc ON s.s_id = sc.s_id
GROUP BY s.s_id, s.s_name
HAVING AVG(sc.s_score) >= 85;
④ 可能的坑:聚合过滤不能用 WHERE
第 33 题:各课程平均分排序(不重要)
① 题目本质:排序优先级
② 解题关键:ORDER BY avg ASC, c_id DESC(多字段排序,靠前优先级高)
③ 核心 SQL
SELECT c_id, FORMAT(AVG(s_score), 2) avg
FROM Score
GROUP BY c_id
ORDER BY avg ASC, c_id DESC;
④ 可能的坑:ORDER BY 多字段排序以靠近 ORDER BY 的字段优先
第 34 题:数学不及格的学生(不重点)
① 题目本质:多表联接过滤
② 解题关键:联表拿到课程名后 WHERE c_name = '数学' AND s_score < 60
③ 核心 SQL
SELECT s.s_name, sc.s_score
FROM Student s
INNER JOIN Score sc ON s.s_id = sc.s_id
INNER JOIN Course c ON sc.c_id = c.c_id
WHERE c.c_name = '数学'
AND sc.s_score < 60;
④ 可能的坑:USING(col) 是 ON t1.col = t2.col 的语法糖,字段名相同时可用
第 35 题:所有学生的课程及分数(行转列重点)⭐
① 题目本质:行转列(Pivot)
② 解题关键:MAX(CASE WHEN c_name = 'XX' THEN s_score END) + GROUP BY s_name;用 LEFT JOIN 保留无选课学生
③ 核心 SQL
SELECT s.s_name,
MAX(CASE WHEN c_name = '语文' THEN s_score END) `语文`,
MAX(CASE WHEN c_name = '数学' THEN s_score END) `数学`,
MAX(CASE WHEN c_name = '英语' THEN s_score END) `英语`,
MAX(CASE WHEN c_name = '化学' THEN s_score END) `化学`
FROM Student s
LEFT JOIN Score sc USING (s_id)
LEFT JOIN Course c USING (c_id)
GROUP BY s.s_name;
④ 可能的坑
- 不加
MAX(或其他聚合)会报only_full_group_by错误,且展示一堆 NULL 行 INNER JOIN会丢掉没选课的学生,必须LEFT JOIN
想一想:为什么
CASE WHEN行转列必须配MAX(或其他聚合函数)?
GROUP BY之后每个学生只输出一行,但CASE WHEN c_id = '语文'只在「语文课那一行」有值、其余行为 NULL。不加聚合会触发only_full_group_by报错,且每个学生被拆成多行。MAX会在一堆 NULL 和唯一一个真实分数里挑出那个真实值,把「行」压成「列」。
第 36 题:任意一门课成绩 > 70 的学生(重点)⭐
① 题目本质:行过滤,存在即入选
② 解题关键:INNER JOIN + WHERE s_score > 70(只要有一行满足条件即可)
③ 核心 SQL
SELECT s.s_name, c.c_name, sc.s_score
FROM Student s
INNER JOIN Score sc USING (s_id)
INNER JOIN Course c USING (c_id)
WHERE sc.s_score > 70;
④ 可能的坑:同一学生多门课 > 70 会出现多行,看需求决定是否 DISTINCT
第 37 题:不及格课程按课程号降序(不重点)
① 题目本质:过滤 + 排序
② 解题关键:WHERE s_score < 60 ORDER BY c_id DESC
③ 核心 SQL
SELECT s_id, c_id
FROM Score
WHERE s_score < 60
ORDER BY s_id, c_id DESC;
④ 可能的坑:多字段排序时 s_id 默认 ASC,c_id 明确 DESC
第 38 题:03 课程成绩 > 80 的学生(不重要)
① 题目本质:联表 + 双条件过滤
② 解题关键:WHERE c_id = '03' AND s_score > 80
③ 核心 SQL
SELECT s.s_id, s.s_name
FROM Student s
INNER JOIN Score sc USING (s_id)
WHERE sc.c_id = '03'
AND sc.s_score > 80;
④ 可能的坑:>80 还是 >=80,仔细看题目数字
第 39 题:每门课程的学生人数(不重要)
① 题目本质:分组计数
② 解题关键:GROUP BY c_id + COUNT(DISTINCT s_id)
③ 核心 SQL
SELECT c_id, COUNT(DISTINCT s_id) student_count
FROM Score
GROUP BY c_id;
④ 可能的坑:COUNT(*) vs COUNT(s_id),有 NULL 时结果不同
第 40 题:张三老师课程中成绩最高的学生(重要,Top1)⭐
① 题目本质:多表联接 + 取最大值 Top1
② 解题关键:联表过滤出张三的课程后 ORDER BY DESC LIMIT 1
③ 核心 SQL
SELECT s.s_name, sc.s_score
FROM Student s
INNER JOIN Score sc USING (s_id)
INNER JOIN Course c USING (c_id)
INNER JOIN (SELECT * FROM Teacher WHERE t_name = '张三') t USING (t_id)
ORDER BY sc.s_score DESC
LIMIT 0, 1;
④ 可能的坑
LIMIT 0, 1从第 0 行开始取 1 条(即第 1 名)- 并列第一会被随机取一个,需确认业务是否允许
第 41 题:不同课程成绩相同的学生(重点,自连接)⭐
① 题目本质:存在相同分值但来自不同课程
② 解题关键:子查询找出”同分且来自多门课”的分数,再 JOIN 回去
③ 核心 SQL
SELECT sc.s_id, sc.c_id, sc.s_score
FROM Score sc
INNER JOIN (SELECT s_score
FROM Score
GROUP BY s_score
HAVING COUNT(*) > 1
AND COUNT(DISTINCT c_id) > 1) scc
ON sc.s_score = scc.s_score
ORDER BY sc.s_score DESC;
自连接补充(某学生某课成绩是否有人比他低):
SELECT s1.*, s2.*
FROM Score s1
JOIN Score s2 ON s1.c_id = s2.c_id AND s2.s_score > s1.s_score
ORDER BY s1.s_id;
④ 可能的坑:自连接使用同一张表两个别名,务必在 ON 条件中限制清楚关联逻辑
第 43 题:选修人数 > 5 的课程统计(不重要)
① 题目本质:聚合过滤 + 多字段排序
② 解题关键:HAVING cnt > 5;ORDER BY cnt DESC, c_id ASC
③ 核心 SQL
SELECT c_id, COUNT(s_id) cnt
FROM Score
GROUP BY c_id
HAVING cnt > 5
ORDER BY cnt DESC, c_id ASC;
④ 可能的坑:HAVING 可以引用 SELECT 别名(MySQL 允许),但标准 SQL 不保证
第 44 题:至少选修两门课的学生(不重要)
① 题目本质:分组计数 >= 2
② 解题关键:HAVING COUNT(c_id) >= 2
③ 核心 SQL
SELECT s_id, COUNT(c_id) cnt
FROM Score
GROUP BY s_id
HAVING cnt >= 2;
④ 可能的坑:同上
第 45 题:选修了全部课程的学生(重点)⭐
① 题目本质:选课数 = 总课程数
② 解题关键:子查询统计每人选课数,与 COUNT(*) FROM Course 相等则入选
③ 核心 SQL
SELECT s.*
FROM Student s
INNER JOIN (SELECT s_id
FROM Score
GROUP BY s_id
HAVING COUNT(s_id) = (SELECT COUNT(*) FROM Course)) scc
ON s.s_id = scc.s_id;
④ 可能的坑:INNER JOIN 取交集,天然排除未选课学生
第 46 题:查询各学生年龄(日期函数)
① 题目本质:日期计算
② 解题关键:TIMESTAMPDIFF(YEAR, s_birth, CURDATE()) 是最精确的年龄算法
③ 核心 SQL
-- 推荐写法
SELECT s_name, s_birth,
TIMESTAMPDIFF(YEAR, s_birth, CURDATE()) age
FROM Student;
-- 身份证号计算年龄(进阶)
SELECT id_card,
TIMESTAMPDIFF(YEAR,
CASE
WHEN LENGTH(id_card) = 18 THEN STR_TO_DATE(SUBSTR(id_card, 7, 8), '%Y%m%d')
WHEN LENGTH(id_card) = 15 THEN STR_TO_DATE(CONCAT('19', SUBSTR(id_card, 7, 6)), '%Y%m%d')
END,
CURDATE()) age
FROM Student;
④ 可能的坑
DATEDIFF / 365不精确,TIMESTAMPDIFF(YEAR,...)自动处理闰年DATEDIFF(a, b)是 a - b(左减右),注意顺序SUBSTR下标从 1 开始,不是 0CASE ... END不写END会报 1064 语法错误
第 47 题:没学过张三任一门课的学生(自己写的)
① 题目本质:没有与张三任何课程有交集
② 解题关键:NOT EXISTS 双层嵌套——外层遍历学生选课,内层判断是否是张三的课
③ 核心 SQL
SELECT s.s_name
FROM Student s
WHERE NOT EXISTS (
SELECT 1
FROM Score sc
WHERE s.s_id = sc.s_id
AND EXISTS (
SELECT 1
FROM Course c
INNER JOIN Teacher t ON c.t_id = t.t_id
WHERE t.t_name = '张三'
AND sc.c_id = c.c_id
)
);
④ 可能的坑
- 与第 5 题(没学过张三”全部”课)不同,本题是”任一”,反而更严格(一门都没选)
第 48 题:两门以上不及格的学生(自己写的)
① 题目本质:同第 15 题变体
② 解题关键:HAVING SUM(s_score < 60) > 2
③ 核心 SQL
SELECT s_id, ROUND(AVG(s_score), 2) avg_score
FROM Score
GROUP BY s_id
HAVING SUM(s_score < 60) > 2;
④ 可能的坑:注意 > 2 是超过两门(即 3 门及以上),题意需要 >= 2 还是 > 2 看题
第 49 题:本月过生日的学生(日期函数)
① 题目本质:月份匹配
② 解题关键:MONTH(s_birth) = MONTH(CURDATE())
③ 核心 SQL
SELECT s_name, s_birth
FROM Student
WHERE MONTH(s_birth) = MONTH(CURDATE());
④ 可能的坑:DISTINCT 对整行去重,不能只对某一列去重
第 50 题:下周过生日的学生(日期函数进阶)
① 题目本质:7 天内的生日匹配,需处理跨年边界
② 解题关键:将生日换成今年日期后 BETWEEN curdate AND curdate + 7;跨年再加一个明年的判断
③ 核心 SQL
SELECT s_name, s_birth
FROM Student
WHERE STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-', DATE_FORMAT(s_birth, '%m-%d')), '%Y-%m-%d')
BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY)
OR STR_TO_DATE(CONCAT(YEAR(CURDATE()) + 1, '-', DATE_FORMAT(s_birth, '%m-%d')), '%Y-%m-%d')
BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY);
下个月过生日:
SELECT s_name, s_birth
FROM Student
WHERE MONTH(s_birth) = MONTH(DATE_ADD(CURDATE(), INTERVAL 1 MONTH));
④ 可能的坑
- 不要用
WEEK()函数——MySQLWEEK有 0/1 两种模式(从周日还是周一开始),且跨年时周数计算错乱 - 必须考虑 12-30、12-31 这种跨年生日,需
OR明年判断
想一想:为什么不能直接用
s_birth BETWEEN CURDATE() AND CURDATE()+7?为什么还要OR一个「明年」分支?因为
s_birth存的是出生年月日,可能是几十年前的日期,直接和今年的日期比永远落在区间外。正确做法是先把它「平移」到今年再比;而 12 月底过生日的人,今年的日期已经过了,只能落到明年,所以要再加一个YEAR + 1的分支兜住跨年。
附录:核心规律速查
| 场景 | 推荐方式 |
|---|---|
| 存在/不存在 | EXISTS / NOT EXISTS |
| 集合包含比较 | NOT EXISTS (NOT EXISTS ...) |
| 集合大小比较 | COUNT(DISTINCT ...) |
| 分组内排名 | ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) |
| 行转列 | MAX(CASE WHEN ... THEN ... END) + GROUP BY |
| 分区间统计 | SUM(CASE WHEN ... THEN 1 ELSE 0 END) |
| Top1 | ORDER BY DESC LIMIT 1 |
| TopN | 窗口函数 + 外层 WHERE rk <= N |
| 年龄计算 | TIMESTAMPDIFF(YEAR, birth, CURDATE()) |
| 字符串集合比较 | GROUP_CONCAT(... ORDER BY ... SEPARATOR ',') |
| 多字段排序 | ORDER BY a DESC, b ASC(靠前优先级高) |
INvsEXISTS黄金法则:子查询结果可能含 NULL 时一律用EXISTS;外表大内表小用IN,外表小内表大用EXISTS;能用INNER JOIN ON就不用IN。
章末提问
1. 「NOT IN 子查询里为什么一旦出现 NULL 结果就整表为空?怎么规避?」
结论:因为 x NOT IN (1, NULL) 展开成 x != 1 AND x != NULL,而 x != NULL 恒为未知,WHERE 不保留未知行,所以整表被过滤空。规避:对可能含 NULL 的子查询一律改用 NOT EXISTS。
2. 「WHERE 和 HAVING 到底差在哪?为什么聚合条件不能写 WHERE?」
结论:WHERE 在分组前逐行过滤,HAVING 在分组后对聚合结果过滤。因为 AVG/SUM 这类聚合值只有在 GROUP BY 之后才存在,分组前无从比较,所以过滤聚合结果只能用 HAVING。
3. 「ROW_NUMBER、RANK、DENSE_RANK 三者区别?并列成绩时该用哪个?」
结论:ROW_NUMBER 连续编号、并列被拆开;RANK 并列同号且下一名跳号;DENSE_RANK 并列同号但不跳号。要并列同名次用 RANK/DENSE_RANK,要严格取固定人数用 ROW_NUMBER。
4. 「行转列为什么 CASE WHEN 必须配 MAX?不配会怎样?」
结论:因为 GROUP BY 后每个学生只剩一行,而 CASE WHEN 对每个学生只在「该门课」那行有值、其余为 NULL,必须靠 MAX 在一堆 NULL 里挑出唯一真实值压成列。不配聚合会触发 only_full_group_by 报错或散成多行。
5. 「COUNT(*) 和 COUNT(列) 有什么区别?」
结论:COUNT(*) 数行数、把 NULL 行也算进去;COUNT(列) 数该列非 NULL 的值个数。左连接补出来的 NULL 行,用 COUNT(*) 会误算,必须用 COUNT(具体列)。