MySQL——SQL 经典练习题:学生选课成绩查询(附详细注释)

核心知识点

在开始做这 50 道题之前,先花几分钟把下面这些 SQL 基础知识点过一遍。它们几乎贯穿了后面每一道题,理解了它们,做题会顺畅很多。

1. 多表连接(JOIN)

多表连接用于把多张表的数据按某个关联字段拼在一起。最常用的三种:

  • INNER JOIN(内连接):只保留两表都能匹配上的记录,匹配不上的直接丢弃。
  • LEFT JOIN(左连接):以左表为准,左表所有记录都保留,右表匹配不到时字段为 NULL。
  • RIGHT JOIN(右连接):以右表为准,右表所有记录都保留,左表匹配不到时字段为 NULL。

连接条件写在 ON 后面,例如 ON a.Sid = b.Sid,表示按学生编号把两张表关联起来。

2. 子查询

子查询就是嵌套在 SELECT、WHERE、FROM 等位置里的另一个查询。它可以把复杂问题拆成多个简单步骤:先查出中间结果,再拿这个结果去参与外层查询。常见用法有 WHERE Sid IN (子查询)FROM (子查询) AS 别名 等。

3. 分组聚合(GROUP BY + 聚合函数 + HAVING)

  • GROUP BY:按某个字段把数据分成若干组,例如按学生编号分组,每个学生一组。
  • 聚合函数:对每组做统计,常用有 COUNT(计数)、SUM(求和)、AVG(平均)、MAX(最大)、MIN(最小)。
  • HAVING:在分组之后对组进行过滤,例如筛选平均分大于等于 60 的分组。注意 WHERE 是分组前过滤行,HAVING 是分组后过滤组。

4. 去重(DISTINCT)

DISTINCT 用于去掉查询结果中重复的行。例如一个学生有多条成绩记录,关联学生表后学生信息会重复出现,用 SELECT DISTINCT 可以只保留一份。

5. 模糊查询(LIKE 通配符)

LIKE 配合通配符做模糊匹配:

  • %:匹配任意长度的字符串,LIKE '李%' 匹配所有以「李」开头的姓名,LIKE '%风%' 匹配任意位置含「风」的姓名。
  • _:匹配单个字符。

6. 排序(ORDER BY)

ORDER BY 用于对结果排序,ASC 升序(默认),DESC 降序。可以按多个字段排序,例如 ORDER BY avg_score DESC, Cid ASC 表示先按平均分降序,平均分相同时再按课程编号升序。

7. 条件统计(CASE WHEN)

CASE WHEN 可以在查询里做条件判断,常用于按条件统计人数。例如统计及格人数:SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END),满足条件记 1,不满足记 0,再求和就是人数。

8. 窗口函数(MySQL 8.0+)

窗口函数可以在不合并分组的情况下,对每一行计算排名、平均分等,常用于「每门课程排名第几」「取前几名」这类题目。常用三个:

  • ROW_NUMBER():按顺序编号,同分也会依次编号,名次不并列。
  • RANK():同分名次相同,但后续名次会空缺,例如 100(1)、100(1)、90(3)。
  • DENSE_RANK():同分名次相同,后续名次不空缺,例如 100(1)、100(1)、90(2)。

窗口函数配合 OVER(PARTITION BY 字段 ORDER BY 字段) 使用,PARTITION BY 表示按什么分组,ORDER BY 表示组内怎么排序。

9. 常用函数

  • ROUND(字段, 2):保留两位小数。
  • YEAR(日期字段):取出年份,例如判断 1990 年出生。
  • MONTH(日期字段):取出月份,例如判断本月、下月过生日。
  • CONCAT(字符串1, 字符串2):拼接字符串,常用于拼接百分比。
  • GROUP_CONCAT(字段):把分组内多个值拼成一个字符串,常用于比较选课是否完全相同。

把这些知识点记牢,后面 50 道题基本就是它们的组合应用。下面我们正式开始逐题讲解。

一、题目背景与数据表结构

本文整理了 50 道经典的 SQL 查询练习题,围绕学生选课与成绩管理场景,覆盖了子查询、多表连接(INNER JOIN、LEFT JOIN、RIGHT JOIN)、分组聚合(GROUP BY、HAVING)、去重(DISTINCT)以及模糊查询(LIKE)等核心知识点。每道题都配有详细的注释,方便初学者理解 SQL 的执行逻辑。

练习涉及以下四张表:

  • Student(学生表):Sid 学生编号,Sname 姓名,Sage 生日,Ssex 性别
  • Course(课程表):Cid 课程编号,Cname 课程名,Tid 教师编号
  • SC(成绩表):Sid 学生编号,Cid 课程编号,score 分数
  • Teacher(教师表):Tid 教师编号,Tname 教师姓名

二、查询"01"课程比"02"课程成绩高的学生信息

这道题的核心思路是:先分别取出所有学生"01"课程和"02"课程的成绩,再通过内连接把同一个学生的两门课成绩拼在一起,最后用 WHERE 条件筛选出"01"课程分数大于"02"课程分数的学生。

sql 复制代码
-- a子查询:取出所有学生01课程的成绩
-- b子查询:取出所有学生02课程的成绩
-- a、b内连接,拿到同时选01、02两门课的学生
-- 筛选条件:01课程分数 > 02课程分数
-- 关联Student表获取学生完整信息
SELECT
    stu.*,
    a.score AS score_01,  -- 别名:01课程的分数
    b.score AS score_02   -- 别名:02课程的分数
FROM
    (SELECT Sid, score FROM SC WHERE Cid = '01') a
INNER JOIN
    (SELECT Sid, score FROM SC WHERE Cid = '02') b
ON a.Sid = b.Sid  -- 通过学生编号匹配同一个学生的两门课成绩
INNER JOIN Student stu
ON a.Sid = stu.Sid  -- 关联学生表,获取学生信息
WHERE a.score > b.score;  -- 条件:01课程成绩大于02课程成绩

三、查询同时存在"01"课程和"02"课程的情况

这道题只需要找出同时选修了"01"和"02"两门课程的学生编号。思路是分别取出选"01"课程和选"02"课程的学生编号,再用内连接取交集,只有两边都存在的 Sid 才会被保留。

sql 复制代码
-- t1:选了01课程的学生编号
-- t2:选了02课程的学生编号
-- 内连接,只有两边都存在的Sid才会显示,代表同时选两门课
SELECT t1.Sid
FROM
    (SELECT Sid FROM SC WHERE Cid='01') t1
INNER JOIN
    (SELECT Sid FROM SC WHERE Cid='02') t2
ON t1.Sid = t2.Sid;  -- 学生编号相等,代表同一个学生同时选01、02

四、查询存在"01"课程但可能不存在"02"课程的情况

这道题要求保留所有选了"01"课程的学生,即使他们没有选"02"课程也要显示出来,没选"02"课程时成绩显示为 NULL。这里使用左连接(LEFT JOIN),以选"01"课程的学生为左表,右表匹配不到时字段自动为 NULL。

sql 复制代码
-- 左连接:左表t1是选01课程的全部学生
-- 右表t2匹配02课程成绩,没有02课程记录时,t2字段全部为NULL
SELECT
    t1.Sid,
    t1.score AS score_01,  -- 01课程分数,一定有值
    t2.score AS score_02   -- 02课程分数,没选就是NULL
FROM
    (SELECT Sid, score FROM SC WHERE Cid='01') t1
LEFT JOIN
    (SELECT Sid, score FROM SC WHERE Cid='02') t2
ON t1.Sid = t2.Sid;  -- 通过学生id匹配,保留所有选01的学生

五、查询不存在"01"课程但存在"02"课程的情况

这道题与上一题相反,要求找出选了"02"课程但没有选"01"课程的学生。使用右连接(RIGHT JOIN),以选"02"课程的学生为基础,再通过 WHERE t1.Sid IS NULL 过滤掉那些在左表(选"01"课程)中匹配不到的学生。

sql 复制代码
-- t1:选01课程学生;t2:选02课程学生
-- 右连接,以选02课程学生为基础
-- WHERE t1.Sid IS NULL:左表匹配不到,代表该学生没有选01课程
SELECT t2.*
FROM
    (SELECT Sid,score FROM SC WHERE Cid='01') t1
RIGHT JOIN
    (SELECT Sid,score FROM SC WHERE Cid='02') t2
ON t1.Sid = t2.Sid
WHERE t1.Sid IS NULL;  -- t1没有匹配数据,说明没有学习01课程

六、查询平均成绩大于等于 60 分的同学信息

这道题考察分组聚合与分组后过滤。先用 GROUP BY 按学生分组,用 AVG 求平均分,再用 HAVING 在分组后过滤出平均分大于等于 60 分的学生,最后关联学生表获取完整信息。

sql 复制代码
-- ROUND(字段,2):保留2位小数
-- GROUP BY:按学生编号、姓名分组,每个学生一组
-- HAVING:分组之后过滤,筛选平均分>=60的分组
SELECT
    stu.Sid,
    stu.Sname,
    ROUND(AVG(sc.score),2) AS avg_score -- AVG求平均分,ROUND保留2位小数
FROM Student stu
INNER JOIN SC sc
ON stu.Sid = sc.Sid  -- 学生表关联成绩表
GROUP BY stu.Sid, stu.Sname  -- 根据学生分组
HAVING AVG(sc.score) >= 60; -- 分组后过滤:平均成绩大于等于60

七、查询在 SC 表存在成绩的学生信息

这道题比较简单,只要学生在成绩表 SC 中有记录,就说明他有成绩。使用 INNER JOIN 关联学生表和成绩表,再用 DISTINCT 去重,避免一个学生有多条成绩记录时学生信息重复出现。

sql 复制代码
-- DISTINCT:去重,一个学生有多条成绩记录,只保留一份学生信息
SELECT DISTINCT stu.*
FROM Student stu
INNER JOIN SC sc
ON stu.Sid = sc.Sid; -- 学生表关联成绩表,只要SC有记录就代表有成绩

八、查询所有同学的选课总数和总成绩

这道题要求显示所有学生,包括没有选课的学生(统计字段显示为 NULL)。思路是先用子查询按学生编号分组统计选课数量和总成绩,再用左连接把统计结果关联到所有学生上,保证每个学生都显示。

sql 复制代码
-- Student左连接统计子查询,保证所有学生都显示,没有选课统计字段为null
-- 子查询b:按学生sid分组,统计选课数量、成绩总和
SELECT
    a.Sid,
    a.Sname,
    b.course_count,   -- 选课总数
    b.sum_score       -- 所有课程总成绩,没选课则为null
FROM Student AS a
LEFT JOIN (
    SELECT
        Sid,
        COUNT(1) AS course_count, -- COUNT(1)统计该学生选课记录行数=选课总数
        SUM(score) AS sum_score   -- SUM累加分数,得到总成绩
    FROM SC
    GROUP BY Sid  -- 根据学生编号分组统计
) AS b
ON a.Sid = b.Sid; -- 左连接,所有学生全部保留,无选课则b字段为null

九、查询「李」姓老师的数量

这道题考察模糊查询。LIKE '李%' 中的 % 是通配符,可以匹配任意长度的字符串,因此能匹配所有以"李"开头的老师姓名,再用 COUNT 统计数量。

sql 复制代码
-- LIKE '李%':%是通配符,匹配以李开头的字符串
-- COUNT统计符合条件老师条数
SELECT COUNT(Tid) AS li_teacher_count
FROM Teacher
WHERE Tname LIKE '李%';

十、查询学过「张三」老师授课的同学的信息

这道题涉及四张表的连续关联。思路是:教师表关联课程表找到张三教的课程,课程表关联成绩表找到上这些课的学生编号,成绩表再关联学生表拿到学生的完整信息。

sql 复制代码
-- Teacher关联Course:拿到张三老师教的课程编号
-- Course关联SC:拿到学习该课程的学生编号
-- SC关联Student:拿到学生完整信息
SELECT d.*
FROM Teacher AS a
INNER JOIN Course AS b
ON a.Tid = b.Tid  -- 教师编号关联课程的教师编号,找到张三教的课
INNER JOIN SC AS c
ON b.Cid = c.Cid  -- 课程编号关联成绩表,选出上这门课的学生
INNER JOIN Student AS d
ON c.Sid = d.Sid  -- 学生编号关联学生表,获取学生全部信息
WHERE a.Tname = '张三'; -- 筛选老师名字为张三

十一、查询没有学全所有课程的同学的信息

这道题先统计每个学生的选课数量,再与课程总数比较。子查询中先用 GROUP BY 按学生分组统计选课数,用 HAVING 过滤出选课数小于课程总数的学生,最后关联学生表获取完整信息。

sql 复制代码
-- 子查询a:统计每个学生选课数目;(SELECT COUNT(Cid) FROM Course)获取课程总数量
-- HAVING:筛选选课数 < 课程总数量,代表没有学完全部课程
-- 关联Student拿到学生信息
SELECT b.*
FROM (
    SELECT
        Sid,
        COUNT(1) AS select_course_num -- COUNT统计该学生选了几门课
    FROM SC
    GROUP BY Sid
    HAVING select_course_num < (SELECT COUNT(Cid) FROM Course) -- 选课数小于全部课程总数
) AS a
INNER JOIN Student AS b
ON a.Sid = b.Sid; -- 关联学生表获取学生完整信息

十二、查询至少有一门课与学号为"01"的同学所学相同的同学的信息

这道题先查出"01"号同学选过的所有课程编号,再找出选过这些课程中任意一门的学生,用 DISTINCT 去重后关联学生表获取完整信息,同时排除"01"号学生本人。

sql 复制代码
-- 思路:先查出01号同学选过的所有课程cid,再找出选过这些cid任意一个的学生,去重,关联学生表获取信息
SELECT DISTINCT stu.*
FROM Student stu
INNER JOIN SC sc
ON stu.Sid = sc.Sid
WHERE sc.Cid IN (
    -- 子查询:获取学号01同学学过的所有课程编号
    SELECT Cid FROM SC WHERE Sid = '01'
)
AND stu.Sid != '01'; -- 排除01号学生自己

十三、查询和"01"号的同学学习的课程完全相同的其他同学的信息

这道题要求找出与"01"号同学选课完全相同的其他学生。思路分三步:先排除选过"01"号没选过的课的学生,再保证选课总数与"01"号一致,最后排除"01"号本人。这里使用 GROUP_CONCAT 把课程编号拼接成字符串进行比较。

sql 复制代码
/*思路:
1. 排除选过01号没选过的课的学生
2. 学生选课总数 = 01号学生选课总数
3. 排除01号本人*/
SELECT b.*
FROM (
    SELECT Sid
    FROM SC
    WHERE Sid != '01'
    GROUP BY Sid
    -- 选课数量等于01同学选课数量
    HAVING COUNT(DISTINCT Cid) = (SELECT COUNT(DISTINCT Cid) FROM SC WHERE Sid='01')
    -- 该学生选的所有课程,全部都在01同学的课程集合内
    AND GROUP_CONCAT(DISTINCT Cid ORDER BY Cid) = (SELECT GROUP_CONCAT(DISTINCT Cid ORDER BY Cid) FROM SC WHERE Sid='01')
) AS a
INNER JOIN Student b
ON a.Sid = b.Sid;

十四、查询没学过"张三"老师讲授的任一门课程的学生姓名

这道题先找到张三老师教的全部课程编号,再找到学过这些课程的学生编号,最后在 Student 表中排除这些学生,剩下的就是没学过张三任何课程的学生。

sql 复制代码
/*思路:
1. 先找到张三老师教的全部课程cid
2. 找到学过这些课程的学生sid
3. 在Student中排除上面这些sid,剩下就是没学过张三任何课的学生*/
SELECT Sname
FROM Student
WHERE Sid NOT IN (
    -- 子查询:学过张三老师任意一门课的学生编号
    SELECT DISTINCT sc.Sid
    FROM Teacher t
    INNER JOIN Course c ON t.Tid = c.Tid
    INNER JOIN SC sc ON c.Cid = sc.Cid
    WHERE t.Tname = '张三');

十五、查询两门及其以上不及格课程的同学的学号,姓名及其平均成绩

这道题先统计每个学生不及格课程的数量,筛选出不及格课程大于等于 2 门的学生,再关联学生表计算整体平均分。注意这里平均分是全部课程的平均分,而不只是不及格课程。

sql 复制代码
/*不及格:score <60
统计每个学生不及格课程数量,>=2条筛选出来,关联学生表,计算整体平均分*/
SELECT
    stu.Sid,
    stu.Sname,
    ROUND(AVG(sc.score),2) AS avg_score -- 整体平均成绩保留2位小数
FROM Student stu
INNER JOIN SC sc
ON stu.Sid = sc.Sid
WHERE sc.score < 60 -- 只看不及格科目
GROUP BY stu.Sid,stu.Sname
HAVING COUNT(1) >=2; -- 不及格课程数量大于等于2门

十六、检索"01"课程分数小于60,按分数降序排列的学生信息

这道题比较简单,先筛选出"01"课程分数小于 60 的记录,再按分数降序排列,同时关联学生表获取学生完整信息。

sql 复制代码
SELECT stu.*, sc.score
FROM Student stu
INNER JOIN SC sc
ON stu.Sid = sc.Sid
WHERE sc.Cid = '01' -- 指定01课程
AND sc.score < 60   -- 分数小于60
ORDER BY sc.score DESC; -- 分数降序排列

十七、按平均成绩从高到低显示所有学生的所有课程的成绩以及平均成绩

这道题先用子查询算出每个学生的平均分,再让学生表关联成绩表,再关联平均分子查询,最后按平均分降序排列,这样每个学生的所有课程成绩和平均分都会显示出来。

sql 复制代码
/*子查询avg_tab:每个学生的平均分数
学生表关联成绩表,再关联平均分子查询,按平均分降序*/
SELECT
    stu.Sid,
    stu.Sname,
    sc.Cid,
    sc.score,
    avg_tab.avg_score
FROM Student stu
INNER JOIN SC sc
ON stu.Sid = sc.Sid
INNER JOIN (
    SELECT Sid, ROUND(AVG(score),2) AS avg_score FROM SC GROUP BY Sid) avg_tab
ON stu.Sid = avg_tab.Sid
ORDER BY avg_tab.avg_score DESC; -- 平均成绩从高到低

十八、查询各科成绩最高分、最低分和平均分

这道题考察分组聚合与条件统计。按课程分组后,用 MAX、MIN、AVG 求最高分、最低分和平均分,再用 CASE WHEN 配合 SUM 统计各分数段人数,计算及格率、中等率、优良率和优秀率,最后按选修人数降序、课程号升序排列。

sql 复制代码
-- 输出:课程ID,课程name,最高分,最低分,平均分,及格率,中等率,优良率,优秀率
-- 及格>=60;中等70-80;优良80-90;优秀>=90
-- 输出课程号、选修人数;结果按人数降序,人数相同按课程号升序
SELECT
    c.Cid,
    c.Cname,
    MAX(sc.score) AS max_score, -- 最高分
    MIN(sc.score) AS min_score, -- 最低分
    ROUND(AVG(sc.score),2) AS avg_score, -- 平均分
    COUNT(sc.Sid) AS select_count, -- 选修人数
    -- 及格率:>=60人数 / 总人数 concat拼接百分号
    CONCAT(ROUND(SUM(CASE WHEN sc.score>=60 THEN 1 ELSE 0 END)/COUNT(*)*100,2),'%') AS pass_rate,
    -- 中等率:70<=score<80
    CONCAT(ROUND(SUM(CASE WHEN sc.score>=70 AND sc.score<80 THEN 1 ELSE 0 END)/COUNT(*)*100,2),'%') AS mid_rate,
    -- 优良率:80<=score<90
    CONCAT(ROUND(SUM(CASE WHEN sc.score>=80 AND sc.score<90 THEN 1 ELSE 0 END)/COUNT(*)*100,2),'%') AS good_rate,
    -- 优秀率:>=90
    CONCAT(ROUND(SUM(CASE WHEN sc.score>=90 THEN 1 ELSE 0 END)/COUNT(*)*100,2),'%') AS excellent_rate
FROM Course c
INNER JOIN SC sc
ON c.Cid = sc.Cid
GROUP BY c.Cid,c.Cname
ORDER BY select_count DESC, c.Cid ASC; -- 选修人数降序,人数相同课程号升序

十九、按各科平均成绩进行排序,并显示排名

这道题提供两种写法:写法一使用变量实现排名,兼容低版本 MySQL,重复名次不跳跃;写法二使用窗口函数 DENSE_RANK(),MySQL 8.0 及以上支持,推荐在 Navicat 中使用。

sql 复制代码
-- 写法1:变量实现排名(兼容低版本MySQL,重复名次不跳跃)
SET @i:=0;
SELECT
    a.Cid,
    a.avg_score,
    (@i:=@i+1) AS r
FROM (
    SELECT
        Cid,
        AVG(score) AS avg_score
    FROM SC
    GROUP BY Cid
    ORDER BY avg_score DESC) AS a;
-- 写法2:窗口函数 DENSE_RANK() MySQL8.0及以上支持,推荐Navicat使用这个
SELECT
Cid,
avg_score,
DENSE_RANK() OVER(ORDER BY avg_score DESC ) AS r
FROM (
SELECT
Cid,
AVG(score) AS avg_score
FROM SC
GROUP BY Cid) AS a;

二十、查询学生的总成绩,并进行排名,总分重复时保留名次空缺

这道题使用窗口函数 RANK() 对总成绩进行排名。RANK() 的特点是重复分数名次相同,但后续名次会空缺,例如 100(1)、100(1)、90(3),会跳过第 2 名。

sql 复制代码
/*RANK():重复分数名次相同,后续名次会空缺
例:100(1),100(1),90(3) 跳过2名*/
SELECT
    Sid,
    sum_Score,
    RANK() OVER(ORDER BY sum_Score DESC) AS r
FROM (
    SELECT
        Sid,
        SUM(score) AS sum_Score
    FROM SC
    GROUP BY Sid) AS a;

二十一、查询学生的总成绩,并进行排名,总分重复时不保留名次空缺

这道题与上一题的区别在于使用窗口函数 DENSE_RANK() 对总成绩进行排名。DENSE_RANK() 的特点是分数相同名次相同,但后续名次不空缺,例如 100(1)、100(1)、90(2),不会跳过名次。RANK() 是有空缺,DENSE_RANK() 无空缺,MySQL 8.0 及以上支持窗口函数。

sql 复制代码
/*DENSE_RANK():分数相同名次相同,后续名次不空缺
示例:100(1),100(1),90(2),不会跳过名次
RANK()是有空缺,DENSE_RANK()无空缺
MySQL8.0及以上支持窗口函数*/
SELECT
    Sid,
    sum_Score,
    DENSE_RANK() OVER(ORDER BY sum_Score DESC) AS r
FROM (
    -- 子查询:按学生编号分组,计算每个学生总成绩
    SELECT
        Sid,
        SUM(score) AS sum_Score
    FROM SC
    GROUP BY Sid) AS a;

二十二、统计各科成绩各分数段人数:课程编号,课程名称,100-8585-7070-6060-0及所占百分比

这道题考察分组聚合与条件统计。按课程分组后,用 CASE WHEN 配合 SUM 统计各分数段人数,再用 CONCAT 拼接百分比。分数段划分:85-100 分、70-85 分、60-70 分、0-60 分。

sql 复制代码
/*分数段划分:
[100-85]:score >=85 AND score <=100
[85-70]:score >=70 AND score <85
[70-60]:score >=60 AND score <70
[60-0]:score >=0 AND score <60
CASE WHEN统计每个分段人数,CONCAT拼接百分比*/
SELECT
    c.Cid,
    c.Cname,
    COUNT(sc.Sid) AS total_stu, -- 当前课程总选修人数
    -- 85-100分数段人数
    SUM(CASE WHEN sc.score >=85 AND sc.score <=100 THEN 1 ELSE 0 END) AS sec_85_100,
    CONCAT(ROUND(SUM(CASE WHEN sc.score >=85 AND sc.score <=100 THEN 1 ELSE 0 END)/COUNT(*)*100,2),'%') AS rate_85_100,
    -- 70-85分数段人数
    SUM(CASE WHEN sc.score >=70 AND sc.score <85 THEN 1 ELSE 0 END) AS sec_70_85,
    CONCAT(ROUND(SUM(CASE WHEN sc.score >=70 AND sc.score <85 THEN 1 ELSE 0 END)/COUNT(*)*100,2),'%') AS rate_70_85,
    -- 60-70分数段人数
    SUM(CASE WHEN sc.score >=60 AND sc.score <70 THEN 1 ELSE 0 END) AS sec_60_70,
    CONCAT(ROUND(SUM(CASE WHEN sc.score >=60 AND sc.score <70 THEN 1 ELSE 0 END)/COUNT(*)*100,2),'%') AS rate_60_70,
    -- 0-60分数段人数
    SUM(CASE WHEN sc.score >=0 AND sc.score <60 THEN 1 ELSE 0 END) AS sec_0_60,
    CONCAT(ROUND(SUM(CASE WHEN sc.score >=0 AND sc.score <60 THEN 1 ELSE 0 END)/COUNT(*)*100,2),'%') AS rate_0_60
FROM Course c
INNER JOIN SC sc
ON c.Cid = sc.Cid
GROUP BY c.Cid,c.Cname; -- 按课程分组统计

二十三、查询各科成绩前三名的记录

这道题使用窗口函数 ROW_NUMBER() 按课程分组,在课程内部按分数降序编号,再筛选编号小于等于 3 的记录。注意 ROW_NUMBER() 对同分也会依次编号,MySQL 8.0 及以上支持。

sql 复制代码
/*窗口函数ROW_NUMBER按课程分组,分数降序编号,取编号<=3;同分也会依次编号;MySQL8.0+可用*/
SELECT t.*
FROM (
    SELECT
        sc.Cid,
        c.Cname,
        sc.Sid,
        stu.Sname,
        sc.score,
        -- PARTITION BY Cid:按课程分组;同一课程内部按分数降序排序编号
        ROW_NUMBER() OVER(PARTITION BY sc.Cid ORDER BY sc.score DESC) AS rn
    FROM SC sc
    INNER JOIN Student stu ON sc.Sid = stu.Sid
    INNER JOIN Course c ON sc.Cid = c.Cid) t
WHERE t.rn <=3; -- 筛选每个课程排名1、2、3

二十四、查询每门课程被选修的学生数

这道题按课程编号分组,用 COUNT 统计该课程有多少条选课记录,即选修学生数量。使用 LEFT JOIN 保证没有学生选修的课程也能显示出来,COUNT(DISTINCT) 防止同一个学生重复选课被重复统计。

sql 复制代码
/*按课程编号分组,count统计该课程有多少条选课记录,即选修学生数量*/
SELECT
    c.Cid,
    c.Cname,
    COUNT(DISTINCT sc.Sid) AS select_student_count -- DISTINCT防止同一个学生重复选课统计
FROM Course c
LEFT JOIN SC sc
ON c.Cid = sc.Cid
GROUP BY c.Cid,c.Cname;

二十五、查询出只选修两门课程的学生学号和姓名

这道题先用子查询按学生编号分组统计选课数量,用 HAVING 筛选出选课数等于 2 的学生,再关联学生表获取姓名。

sql 复制代码
/*子查询按学生sid分组,统计选课数量,having筛选选课数=2;关联学生表获取姓名*/
SELECT a.Sid,a.Sname
FROM Student AS a
INNER JOIN (
    SELECT Sid,COUNT(1) AS num
    FROM SC
    GROUP BY Sid
    HAVING num =2 -- 只选修2门课程
) AS b
ON a.Sid=b.Sid;

二十六、查询男生、女生人数

这道题比较简单,按性别 Ssex 分组,用 COUNT 统计每组人数即可。

sql 复制代码
/*按性别Ssex分组,count统计每组人数*/
SELECT
    Ssex,
    COUNT(1) AS num
FROM Student
GROUP BY Ssex;

二十七、查询名字中含有「风」字的学生信息

这道题考察模糊查询。LIKE '%风%' 中的两个 % 通配符代表字符串任意位置包含「风」字,因此能匹配所有名字中带「风」的学生。

sql 复制代码
/*%通配符:%风%代表字符串任意位置包含"风"*/
SELECT *
FROM Student
WHERE Sname LIKE '%风%';

二十八、查询同名同性学生名单,并统计同名人数

这道题使用自连接,把学生表和自己关联,通过排除自己和自己对比、姓名相同、性别相同三个条件找出同名同性的学生。第一种写法查询名单,第二种写法按姓名和性别分组统计人数。

sql 复制代码
-- ①查询同名同性学生名单
SELECT DISTINCT a.Sid,a.Sname,a.Ssex
FROM Student AS a, Student AS b
WHERE a.Sid != b.Sid       -- 排除自己和自己对比
AND a.Sname=b.Sname        -- 姓名相同
AND a.Ssex=b.Ssex;         -- 性别相同
-- ②统计同名同性的人数
SELECT
b.Sname,
b.Ssex,
COUNT(DISTINCT b.Sid) AS num
FROM Student AS a, Student AS b
WHERE a.Sid != b.Sid
AND a.Sname=b.Sname
AND a.Ssex=b.Ssex
GROUP BY b.Sname,b.Ssex;

二十九、查询 1990 年出生的学生名单

这道题使用 YEAR() 函数取出 Sage 日期字段的年份,再匹配 1990 年。注意 Sage 字段类型是 datetime。

sql 复制代码
/*YEAR()函数取出Sage日期字段的年份,匹配1990
Sage字段类型是datetime*/
SELECT *
FROM Student
WHERE YEAR(Sage) = '1990';

三十、查询每门课程的平均成绩,结果按平均成绩降序排列,平均成绩相同时,按课程编号升序排列

这道题按课程编号分组求平均分,用 ROUND 保留两位小数,再按平均分降序排列,平均分相同时按课程编号升序排列。

sql 复制代码
SELECT
    Cid,
    ROUND(AVG(score),2) AS avg_score -- round保留两位小数看的更清晰
FROM SC
GROUP BY Cid
ORDER BY avg_score DESC, Cid ASC; -- 平均分降序;平均分相同课程号升序

三十一、查询平均成绩大于等于 85 的所有学生的学号、姓名和平均成绩

这道题考察分组聚合与分组后过滤。按学生分组后用 AVG 计算平均分,再用 HAVING 过滤出平均分大于等于 85 的学生,最后用 ROUND 保留两位小数美化输出。

sql 复制代码
/*按学生分组,AVG计算平均分,HAVING过滤分组结果,筛选平均分>=85
ROUND保留2位小数美化输出*/
SELECT
    stu.Sid,
    stu.Sname,
    ROUND(AVG(sc.score),2) AS avg_score
FROM Student stu
INNER JOIN SC sc
ON stu.Sid = sc.Sid
GROUP BY stu.Sid, stu.Sname
HAVING AVG(sc.score) >= 85;

三十二、查询课程名称为「数学」,且分数低于 60 的学生姓名和分数

这道题涉及 Student、SC、Course 三张表的关联。先通过成绩表关联学生表和课程表,再用 WHERE 条件筛选出课程名称为「数学」且分数低于 60 的记录。

sql 复制代码
/*Student、SC、Course三表关联
条件:课程名字是数学,分数<60*/
SELECT
    stu.Sname,
    sc.score
FROM Student stu
INNER JOIN SC sc
ON stu.Sid = sc.Sid
INNER JOIN Course c
ON sc.Cid = c.Cid
WHERE c.Cname = '数学'
AND sc.score < 60;

三十三、查询所有学生的课程及分数情况(存在学生没成绩,没选课的情况)

这道题要求显示所有学生,包括没有选课、没有成绩的学生。使用 Student 左连接 SC,再左连接 Course,保证所有学生全部显示,没选课、没成绩的课程和分数字段为 NULL。

sql 复制代码
/*Student左连接SC,保证所有学生全部显示;没选课、没成绩的课程、分数字段为NULL*/
SELECT
    stu.Sid,
    stu.Sname,
    c.Cname,
    sc.score
FROM Student stu
LEFT JOIN SC sc
ON stu.Sid = sc.Sid
LEFT JOIN Course c
ON sc.Cid = c.Cid;

三十四、查询任何一门课程成绩在 70 分以上的姓名、课程名称和分数

这道题比较简单,通过多表关联拿到学生姓名、课程名称和分数,再用 WHERE 条件筛选出分数大于 70 的记录。

sql 复制代码
-- 条件分数>70,多表关联拿到姓名、课程名、分数
SELECT
    stu.Sname,
    c.Cname,
    sc.score
FROM Student stu
INNER JOIN SC sc
ON stu.Sid = sc.Sid
INNER JOIN Course c
ON sc.Cid = c.Cid
WHERE sc.score > 70;

三十五、查询不及格的课程

这道题考察多表关联与条件筛选。不及格即分数小于 60,通过成绩表关联课程表,输出学生编号、课程编号、课程名称和分数。

sql 复制代码
-- 不及格 score <60,输出学生id、课程id、课程名字、分数
SELECT
    sc.Sid,
    sc.Cid,
    c.Cname,
    sc.score
FROM SC sc
INNER JOIN Course c
ON sc.Cid = c.Cid
WHERE sc.score < 60;

三十六、查询课程编号为 01 且课程成绩在 80 分以上的学生的学号和姓名

这道题使用子查询,先找出 01 课程分数大于等于 80 的学生编号,再到 Student 表中匹配这些学生的学号和姓名。

sql 复制代码
-- 子查询找出01课程分数>=80的sid,再去Student表匹配学生信息
SELECT Sid,Sname
FROM Student
WHERE Sid IN (
    SELECT Sid FROM SC WHERE Cid='01' AND score >= 80);

三十七、求每门课程的学生人数

这道题按课程编号分组,用 COUNT 统计每门课程的选课记录数,即选修人数。

sql 复制代码
-- 按课程编号分组,count统计选课记录数,即选修人数
SELECT
    Cid,
    COUNT(1) AS num
FROM SC
GROUP BY Cid;

三十八、成绩不重复的情况下,查询选修「张三」老师所授课程的学生中,成绩最高的学生信息及其成绩

这道题先通过子查询拿到张三老师的 Tid,再拿到张三老师教授的全部课程编号,筛选这些课程的成绩并按分数降序取第一条。因为成绩不重复,所以用 LIMIT 1 取最高分即可。

sql 复制代码
/*1.子查询拿到张三老师的Tid
2.拿到张三老师教授的全部Cid
3.筛选这些课程的成绩,分数降序取第一条(成绩不重复场景)
4.关联Student获取学生完整信息*/
SELECT stu.*, sc.score
FROM SC sc
INNER JOIN Student stu
ON sc.Sid = stu.Sid
WHERE sc.Cid IN (
    SELECT Cid FROM Course
    WHERE Tid = (SELECT Tid FROM Teacher WHERE Tname='张三'))
ORDER BY sc.score DESC
LIMIT 1;

三十九、成绩有重复的情况下,查询选修「张三」老师所授课程的学生中,成绩最高的学生信息及其成绩

这道题与上一题的区别在于成绩可能有重复。如果多个学生并列最高分,LIMIT 1 只会取一条。因此先查出最高分数值,再筛选所有等于最高分的记录,可以取出全部并列高分的学生。

sql 复制代码
/*如果有多个学生并列最高分,limit 1只会拿一条。先查出最高分数值,再筛选所有等于最高分的记录,可以取出全部并列高分学生*/
SELECT stu.*, sc.score
FROM SC sc
INNER JOIN Student stu
ON sc.Sid = stu.Sid
WHERE sc.Cid IN (
    SELECT Cid FROM Course
    WHERE Tid = (SELECT Tid FROM Teacher WHERE Tname='张三'))
AND sc.score = (
    -- 子查询:获取张三老师课程下的最高分数
    SELECT MAX(score) FROM SC
    WHERE Cid IN (SELECT Cid FROM Course WHERE Tid = (SELECT Tid FROM Teacher WHERE Tname='张三')));

四十、查询不同课程成绩相同的学生的学生编号、课程编号、学生成绩

这道题使用自连接,把 SC 表关联两次。通过分数相等、课程编号不同、学生编号不同三个条件找出不同课程成绩相同的记录,再用 DISTINCT 去重,避免重复输出相同记录。

sql 复制代码
/*自连接SC表 a 和 b
a.score=b.score:分数相等
a.Cid != b.Cid:课程编号不同
a.Sid != b.Sid:不是同一个学生
DISTINCT去重,避免重复输出相同记录*/
SELECT DISTINCT a.Sid,a.Cid,a.score
FROM SC AS a,SC AS b
WHERE a.score=b.score
AND a.Cid!=b.Cid
AND a.Sid!=b.Sid;

四十一、查询每门课程成绩最好的前两名

这道题使用窗口函数 ROW_NUMBER() 按课程分组,在课程内部按分数降序编号,再筛选编号小于等于 2 的记录,即可得到每门课程成绩最好的前两名。注意 ROW_NUMBER() 对同分也会依次编号,MySQL 8.0 及以上支持。

sql 复制代码
/*窗口函数ROW_NUMBER按课程分组,分数降序编号,取编号<=2;同分也会依次编号;MySQL8.0+可用*/
SELECT t.*
FROM (
    SELECT
        sc.Cid,
        c.Cname,
        sc.Sid,
        stu.Sname,
        sc.score,
        -- PARTITION BY Cid:按课程分组;同一课程内部按分数降序排序编号
        ROW_NUMBER() OVER(PARTITION BY sc.Cid ORDER BY sc.score DESC) AS rn
    FROM SC sc
    INNER JOIN Student stu ON sc.Sid = stu.Sid
    INNER JOIN Course c ON sc.Cid = c.Cid) t
WHERE t.rn <= 2; -- 筛选每个课程排名1、2

四十二、查询每门课程成绩最好的前两名(允许并列)

这道题与上一题的区别在于使用 DENSE_RANK() 窗口函数,分数相同的学生名次相同,因此可以取出并列的前两名。例如最高分 95 有两人并列第 1,则这两人都会显示。

sql 复制代码
/*DENSE_RANK():分数相同名次相同,后续名次不空缺
可以取出并列的前两名,例如最高分95有两人并列第1,则两人都显示*/
SELECT t.*
FROM (
    SELECT
        sc.Cid,
        c.Cname,
        sc.Sid,
        stu.Sname,
        sc.score,
        -- DENSE_RANK:分数相同名次相同,不跳过名次
        DENSE_RANK() OVER(PARTITION BY sc.Cid ORDER BY sc.score DESC) AS rn
    FROM SC sc
    INNER JOIN Student stu ON sc.Sid = stu.Sid
    INNER JOIN Course c ON sc.Cid = c.Cid) t
WHERE t.rn <= 2; -- 筛选每个课程排名1、2(含并列)

四十三、查询每门课程被选修的学生数(含未选修课程)

这道题按课程编号分组,用 COUNT 统计该课程有多少条选课记录,即选修学生数量。使用 LEFT JOIN 保证没有学生选修的课程也能显示出来,COUNT(DISTINCT) 防止同一个学生重复选课被重复统计。

sql 复制代码
/*按课程编号分组,count统计该课程有多少条选课记录,即选修学生数量
LEFT JOIN保证没有学生选修的课程也能显示,COUNT(DISTINCT)防止重复统计*/
SELECT
    c.Cid,
    c.Cname,
    COUNT(DISTINCT sc.Sid) AS select_student_count -- DISTINCT防止同一个学生重复选课统计
FROM Course c
LEFT JOIN SC sc
ON c.Cid = sc.Cid
GROUP BY c.Cid,c.Cname;

四十四、查询选课数量最多的学生信息

这道题先按学生分组统计选课数量,再用子查询找出最大选课数量,最后筛选出选课数量等于最大值的所有学生,关联学生表获取完整信息。

sql 复制代码
/*1.按学生分组统计选课数量
2.子查询找出最大选课数量
3.筛选选课数量=最大值的所有学生,关联学生表获取信息*/
SELECT stu.*, b.course_count
FROM Student stu
INNER JOIN (
    SELECT Sid, COUNT(1) AS course_count
    FROM SC
    GROUP BY Sid
    HAVING COUNT(1) = (
        -- 子查询:最大选课数量
        SELECT MAX(cnt) FROM (
            SELECT COUNT(1) AS cnt FROM SC GROUP BY Sid) t)
) b
ON stu.Sid = b.Sid;

四十五、查询每门课程的平均成绩,并显示高于平均分的学生

这道题先按课程分组计算平均分,再关联成绩表,筛选出分数高于该课程平均分的学生记录。使用窗口函数 AVG() OVER() 可以在不分组的情况下计算每门课程的平均分。

sql 复制代码
/*窗口函数AVG() OVER(PARTITION BY Cid):按课程分组计算平均分
筛选分数高于该课程平均分的学生*/
SELECT
    sc.Cid,
    c.Cname,
    sc.Sid,
    stu.Sname,
    sc.score,
    ROUND(AVG(sc.score) OVER(PARTITION BY sc.Cid),2) AS avg_score -- 该课程平均分
FROM SC sc
INNER JOIN Student stu ON sc.Sid = stu.Sid
INNER JOIN Course c ON sc.Cid = c.Cid
WHERE sc.score > (
    -- 子查询:该课程平均分
    SELECT AVG(score) FROM SC WHERE Cid = sc.Cid)
ORDER BY sc.Cid, sc.score DESC;

四十六、查询每门课程成绩排名第二的学生

这道题使用窗口函数 ROW_NUMBER() 按课程分组,在课程内部按分数降序编号,再筛选编号等于 2 的记录,即可得到每门课程成绩排名第二的学生。

sql 复制代码
/*窗口函数ROW_NUMBER按课程分组,分数降序编号,取编号=2;MySQL8.0+可用*/
SELECT t.*
FROM (
    SELECT
        sc.Cid,
        c.Cname,
        sc.Sid,
        stu.Sname,
        sc.score,
        -- PARTITION BY Cid:按课程分组;同一课程内部按分数降序排序编号
        ROW_NUMBER() OVER(PARTITION BY sc.Cid ORDER BY sc.score DESC) AS rn
    FROM SC sc
    INNER JOIN Student stu ON sc.Sid = stu.Sid
    INNER JOIN Course c ON sc.Cid = c.Cid) t
WHERE t.rn = 2; -- 筛选每个课程排名第2

四十七、查询每门课程成绩排名第二的学生(允许并列)

这道题与上一题的区别在于使用 DENSE_RANK() 窗口函数,分数相同的学生名次相同,因此可以取出并列的第二名。例如第二名有两人并列,则两人都会显示。

sql 复制代码
/*DENSE_RANK():分数相同名次相同,后续名次不空缺
可以取出并列的第二名,例如第二名有两人并列,则两人都显示*/
SELECT t.*
FROM (
    SELECT
        sc.Cid,
        c.Cname,
        sc.Sid,
        stu.Sname,
        sc.score,
        -- DENSE_RANK:分数相同名次相同,不跳过名次
        DENSE_RANK() OVER(PARTITION BY sc.Cid ORDER BY sc.score DESC) AS rn
    FROM SC sc
    INNER JOIN Student stu ON sc.Sid = stu.Sid
    INNER JOIN Course c ON sc.Cid = c.Cid) t
WHERE t.rn = 2; -- 筛选每个课程排名第2(含并列)

四十八、查询每门课程成绩排名前三的学生(允许并列)

这道题使用 DENSE_RANK() 窗口函数,分数相同的学生名次相同,因此可以取出并列的前三名。例如最高分 95 有两人并列第 1,则这两人都会显示,且后续名次不空缺。

sql 复制代码
/*DENSE_RANK():分数相同名次相同,后续名次不空缺
可以取出并列的前三名,例如最高分95有两人并列第1,则两人都显示*/
SELECT t.*
FROM (
    SELECT
        sc.Cid,
        c.Cname,
        sc.Sid,
        stu.Sname,
        sc.score,
        -- DENSE_RANK:分数相同名次相同,不跳过名次
        DENSE_RANK() OVER(PARTITION BY sc.Cid ORDER BY sc.score DESC) AS rn
    FROM SC sc
    INNER JOIN Student stu ON sc.Sid = stu.Sid
    INNER JOIN Course c ON sc.Cid = c.Cid) t
WHERE t.rn <= 3; -- 筛选每个课程排名1、2、3(含并列)

四十九、查询本月过生日的学生

这道题使用 MONTH() 函数取出 Sage 日期字段的月份,再与当前月份比较。注意 Sage 字段类型是 datetime,MONTH() 函数可以直接提取月份。

sql 复制代码
/*MONTH()函数取出Sage日期字段的月份,与当前月份比较
Sage字段类型是datetime*/
SELECT *
FROM Student
WHERE MONTH(Sage) = MONTH(NOW());

五十、查询下月过生日的学生

这道题与上一题类似,使用 MONTH() 函数取出 Sage 日期字段的月份,再与当前月份加 1 比较。注意 12 月的时候 +1 会变成 13,简单写法适合练习题;生产环境建议用 DATE_ADD 函数处理跨年情况。

sql 复制代码
/*MONTH()函数取出Sage日期字段的月份,与当前月份+1比较
注意:12月的时候+1会变成13,简单写法适合练习题;生产环境建议用date_add*/
SELECT *
FROM Student
WHERE MONTH(NOW()) + 1 = MONTH(Sage);

十二、总结

这 10 道题覆盖了 SQL 查询中非常核心的几类场景:

  • 多表连接:INNER JOIN、LEFT JOIN、RIGHT JOIN 的适用场景与区别。
  • 子查询:把复杂的查询拆解成多个简单的子查询再组合。
  • 分组聚合:GROUP BY 配合 COUNT、SUM、AVG 等聚合函数,以及 HAVING 做分组后过滤。
  • 去重与模糊查询:DISTINCT 去重、LIKE 通配符匹配。

建议初学者先理解每道题的注释思路,再动手在本地数据库中建表、插入测试数据并逐题运行,这样能更快掌握 SQL 的查询逻辑。

相关推荐
众壹新能源科技1 小时前
经营口径与运维口径不一致时,发电量报表怎么对账
运维·数据库·人工智能
baopixiaoz1 小时前
BeeQuant × BeeAgent:AI驱动智能交易
大数据·人工智能·python·区块链
浔溺1 小时前
al+大数据每日学习笔记33
大数据·笔记·学习
jyOverQ1 小时前
MySQL 索引为什么能加快查询?B+Tree 到底是什么?
数据库·mysql
hfywmsj1 小时前
广州餐饮铺位招租的选址架构:流量入口与成本函数分析
java·大数据·jvm·广州餐饮铺位招租
天天进步20151 小时前
Pixelle-Video 源码解析 #17:TTS 语音生成:Edge-TTS、Index-TTS 如何接入?
前端·数据库
鸽芷咕1 小时前
异构数据同步,凭什么敢说数据不丢?——拆解金仓KFS的全周期一致性校验
数据库
海浪仙人掌1 小时前
账龄分析的定义是什么?账龄分析有什么作用?
数据库
ACP广源盛139246256731 小时前
M6/M5 Pro Mac mini 端侧 AI 落地@ACP#IX6024 PCIe2.0 交换芯片在轻量化 AI 服务中的机会与应用场景
大数据·数据库·人工智能·嵌入式硬件·macos·开源