简介:一份针对SQL初学者与数据库备考者的练习题及答案文档,内容以“学校数据库”为核心应用场景,要求依据给出的表结构创建Student、Course、SC三张表,并完成数据插入、删除、更新及多样查询操作。题型覆盖了单表筛选、年龄与性别组合条件、系科分组统计,以及总分、平均分、最高分、最低分等聚合统计;还包含了连接查询、嵌套子查询、相关子查询以及求平均分前三名、查询每门课程成绩都高于该课程平均分等进阶问题,且每类题目均附有参考答案。资源包内为1个doc文档,压缩后仅43KB,轻量便携,可直接打开浏览或打印刷题。目前已有520人学习下载,适用场景包括课程实验配套练习、SQL考前冲刺、自学巩固或作为教学辅导材料,能帮助读者系统掌握SQL增删改查与多表查询、统计分析的典型写法和解题思路。
1. 一套能直接抄的 SQL 练习题:从建表到嵌套查询的完整闭环
这份《sql语句练习题及答案.doc》是我见过最像课堂实战的 SQL 训练材料,没有之一。它不跟你讲抽象理论,而是直接甩出 school 数据库里的三张表——学生表 Student、课程表 Course、选课表 SC,然后从建表、插数据一路问到嵌套查询、相关子查询和集合运算。换句话说,你拿到的不只是几十道题,而是一整套数据库课程设计级别的 SQL 语法覆盖方案,特别适合准备数据库期末考试、刷面试笔试题、或者刚学完 SQL 语法想找个系统训练场的人。
文档里最值钱的部分不是答案本身,而是题目设计的层次感:先单表查询,再统计分组,然后连接查询逐步升级到嵌套、EXISTS 相关子查询、TOP/ALL 极值查询和集合差集运算。每个阶段恰好卡在 SQL 学习者最容易混淆的节点上。比如grade is null判断缺考成绩、like '_明%'匹配第二个字、having与where的过滤时机差异、left join保留未选课男生等等,这些坑几乎覆盖了从入门到进阶的所有高频失分点。下面我按实际刷题顺序,把这些题目拆开讲透。
2. 建表与基础 DML:把三张表和增删改一次跑通
2.1 数据类型与主键的选择:为什么学号用 char(6) 而不是 int
打开文档第一页,它会先把 Student、Course、SC 的表结构列出来,还特别标了数据类型和长度。这里有个容易被忽略的细节:Sno 学号字段用的是char(6)而不是整数类型。原因是学号通常是固定位数的字符串,可能包含前导零,比如'0001'。如果存成 int,前导零会被丢掉,查询和显示都会出问题。Sname 用varchar(8)因为姓名长度不固定,用变长字符串省空间。Ssex 用char(2)是因为性别就一个字或两个字的固定长度。Sage 用smallint是因为年龄范围很小,没必要用 int 浪费空间。
Ccredit 学分字段用的是tinyint,范围 0 到 255,一张课的学分绝对不会超过这个范围,比 smallint 更省空间。Grade 成绩字段用decimal(12,2),这个值得注意——它允许两位小数,总共 12 位有效数字,可以存类似87.50这样的成绩。如果用 float 或 real 存浮点数,后续做avg(grade)统计时很容易出现 0.30000000000000004 这类精度误差,考试批改时也会因为精度问题被扣分。
创建这三张表的完整语句如下:
create table student ( sno char(6), sname varchar(8), ssex char(2), sage smallint, sdept varchar(15), primary key (sno) ); create table course ( cno char(4), cname varchar(20), cpno char(4), ccredit tinyint, primary key (cno) ); create table sc ( sno char(6), cno char(4), grade decimal(12,2), primary key (sno, cno) );这段代码的逻辑清晰:student 和 course 各自主键独立,而 sc 表的主键是(sno, cno)复合主键,表示一个学生选一门课只能有一条成绩记录。如果要模拟现实中的重修覆盖,就得靠应用层处理,表结构层面复合主键已经杜绝了同一学生对同一课程的重复选课记录。
建表之后顺手插入测试数据。文档里只给了 student 表的两条样例,实际刷题时建议把后面题目需要的数据都补齐,比如插入几条带grade is null的选课记录来测空值查询:
insert into student values ('4001', '赵茵', '男', 20, 'SX'); insert into student values ('4002', '杨华', '女', 21, 'JSJ'); insert into course values ('1001', '数据库原理', '1000', 4); insert into course values ('1002', '数据结构', '1001', 3); insert into sc values ('4001', '1001', 92.50); insert into sc values ('4002', '1002', NULL);insert into sc values ('4002', '1002', NULL)这行是特意留的空值记录,后面第 3 题「查询 1001 课程没有成绩的学生学号」就需要这种数据才能跑出结果。grade is null不等于grade = null,后者永远匹配不到任何行,这个点是 NULL 三值逻辑在 SQL 里的经典陷阱。
2.2 表结构的后期维护:删除表和添加属性
文档里有两道操作题特别像真实业务里的需求:删除整个 student 表和给 student 表添加新列。删除表用drop table student,这是物理删除,表结构、数据、索引全部消失,没有后悔药。如果只是想清空数据保留表结构,应该用delete from student或者truncate table student。delete是 DML 操作,可以加where条件选择性删行,删除后可以回滚;truncate是 DDL 操作,直接释放数据页,速度更快但不能回滚。
给表添加新列的语句如下:
alter table student add sbirthdate datetime;这个操作在考试中出现的频率很高,但很多人会漏了alter table关键字直接写add column。标准 SQL 里add column的column关键字在 SQL Server 和 MySQL 中可省略,但 Oracle 要求必须写add (sbirthdate date)的括号格式。不同数据库方言差异就在这里体现,建议以你实际使用的数据库为准。
删除所有 JSJ 系男生的操作是两条条件的组合,这道题主要考and逻辑和字符串比较。需要注意'JSJ'是带单引号的字符串字面量,不能写成双引号。删除选课记录用子查询嵌套删除,where cno in比直接关联删除更稳妥:
delete from SC where Cno in ( select Cno from Course where Cname = '数据库原理' );这种写法的好处是只需要知道课程名称,不需要提前查课程号。子查询先算出目标课程号集合,外层删除匹配这些课程号的选课记录。如果课程表里有重名课程,这个写法会把所有同名课程的选课记录一起删掉,需要先通过唯一标识确认目标范围。
3. 单表查询与统计分组:从基础语法到聚合函数
3.1 条件过滤与排序:between、like、is null 的边界行为
第 3 题「查询年龄在 19 至 21 岁之间的女生的学号、年龄,按年龄从大到小排列」是典型的范围查询:
select sno, sname, sage from student where ssex = '女' and sage between 19 and 21 order by sage desc;between 19 and 21是闭区间,等价于sage >= 19 and sage <= 21。如果题目要求 19 到 21 不含边界,就得改成sage > 19 and sage < 21。order by sage desc是降序,默认是升序asc。这里有个隐藏考点:order by的列不一定出现在select列表中,比如可以select sno from student order by sage,这是合法的。
第 2 题「查询中第 2 个字为『明』字的学生学号、性别」考的是通配符匹配:
select sno, ssex from student where sname like '_明%';下划线_匹配任意单个字符,百分号%匹配任意长度字符串。所以'_明%'的含义是:第一个字符任意,第二个字符必须是「明」,后面可以跟任意内容。如果想查以「明」开头的名字,应该写'明%';想查包含「明」的名字,写'%明%'。
第 3 题查缺考记录必须用is null而不是= null:
select sno, cno from sc where grade is null and cno = '1001';NULL 在 SQL 里代表未知值,任何与 NULL 的算术比较结果都是 UNKNOWN,where条件只保留 TRUE 的行,UNKNOWN 会被过滤掉。所以查空值只能用is null,查非空用is not null。这道题如果数据里没有 null 的成绩,查询结果会是空集,刷题时最好先插入一条 null 成绩记录。
第 4 题多系别过滤用in列表:
select sno, sname from student where sdept in ('JSJ', 'SX', 'WL') and sage > 25;等价写法是sdept = 'JSJ' or sdept = 'SX' or sdept = 'WL',但in更简洁。注意in列表空值问题:如果列表里包含 NULL,比如sdept in ('JSJ', NULL),匹配行为可能不符合直觉,因为NULL匹配不到任何实际值。这道题文档里的答案带了group by sdept,但严格来说这个查询没有聚合函数,group by不是必需的,按系排列应该用order by sdept。
3.2 聚合函数的正确姿势:group by 与 having 的过滤时机
统计题是这份文档的精华区,第 2 题计算 JSJ 系的平均年龄和最大年龄是聚合函数的基础用法:
select avg(sage), max(sage) from student where sdept = 'JSJ';avg会忽略 NULL 值,如果 sage 有 NULL,计算结果只统计非空行。max同样忽略 NULL。这里如果题目要求「将 NULL 视为 0」,就得先coalesce(sage, 0)再聚合。
第 4 题「计算每一门课的总分、平均分、最高分、最低分,按平均分由高到低排列」是最典型的分组统计:
select cno, sum(grade), avg(grade), max(grade), min(grade) from sc group by cno order by avg(grade) desc;关键点在于:select列表中出现的非聚合列必须出现在group by子句中。这里cno在group by里,所以可以出现在select中。sum(grade)对 NULL 值同样忽略,如果一门课所有人都缺考,sum 会是 NULL,avg 也是 NULL,排序时 NULL 默认排在最前还是最后取决于数据库设置。
第 6 题「查询平均分大于 80 分的学生学号及平均分」引出having:
select sno, avg(grade) from sc group by sno having avg(grade) > 80;where是在分组前过滤行,having是在分组后过滤组。这里如果写成where avg(grade) > 80是语法错误,聚合函数不能出现在where里。什么时候用where什么时候用having,最简单的判断方式是问自己:这个条件是针对原始行的还是针对分组结果的。针对原始行用where,针对聚合结果用having。
第 10 题「统计有大于两门课不及格的学生学号」是having加count的进阶:
select sno from sc where grade < 60 group by sno having count(*) > 2;先通过where grade < 60过滤出所有不及格的记录,再按学号分组,count(*)统计每个学生不及格的门数,having count(*) > 2只保留超过两门的学生。注意where和having的执行顺序在逻辑上是先where再group by再having,先缩小数据范围再做分组运算,性能更好。
第 8 题「统计有 10 位成绩大于 85 分以上的课程号」特别容易理解错:
select cno from sc where grade > 85 group by cno having count(*) = 10;它查的不是某门课有 10 个人且都大于 85,而是某门课大于 85 分的人数恰好等于 10。如果一门课有 12 个人大于 85,count(*) = 10就不成立,换成了count(*) >= 10才能包含。这类题做错通常是因为把题意的「10 位」理解成课程编号相关的条件。
3.3 十档分制换算:用算术表达式生成新列
第 5 题「按 10 分制查询成绩」这道题很多人第一次见会觉得绕,实际是考算术表达式和除法运算的类型转换:
select sno, cno, grade / 10.0 + 1 as level from sc;这里的关键是写grade / 10.0而不是grade / 10。如果 grade 是整数类型的列,整数除以整数会得到整数,比如85 / 10 = 8,丢掉小数部分,+ 1就是 9 档。而85 / 10.0 = 8.5,+ 1就是 9.5 档,然后可以配合取整函数或floor处理。原题的参考答案直接用grade/10.0+1,没有处理边界和取整,实际使用时建议加一层floor或cast把结果规整到整数档位:
select sno, cno, floor(grade / 10.0) + 1 as level from sc;floor(grade / 10.0)把 85 分变成 8,加 1 得到 9 档。100 分的情况是floor(100 / 10.0) + 1 = 11档,但 10 分制最高只到 10 档,需要再套一层case when处理满分边界。这类算术表达式生成新列在真实报表里非常常见,比如把百分制成绩映射为优良中差等级。
4. 连接查询实战:内连接、左连接与多表条件过滤
4.1 等值连接与嵌套子查询的等价互换
连接查询部分的第一道题「查询 JSJ 系的学生选修的课程号」是最基础的等值连接:
select distinct cno from student, sc where student.sno = sc.sno and sdept = 'JSJ';这里用了隐式连接写法,逗号分隔两表,where里写连接条件。现代 SQL 更推荐显式inner join,可读性更强:
select distinct sc.cno from student inner join sc on student.sno = sc.sno where student.sdept = 'JSJ';distinct是必须的,因为一个 JSJ 系学生可能选修多门课,没有distinct会出现重复课程号。这道题也考了列限定符的使用——sno在两个表里都有,必须写成student.sno = sc.sno才能消歧义。
第 2 题「查询选修 1002 课程的学生的学生(不用嵌套及嵌套 2 种方法)」是连接和子查询的对比题:
-- 方法一:连接 select sname from student, sc where student.sno = sc.sno and cno = '1002'; -- 方法二:嵌套 select sname from student where sno in ( select sno from sc where cno = '1002' );两种方法结果集相同但执行路径不同。嵌套子查询的思路是先查找到选了 1002 课的学生学号集合,再回到 student 表过滤姓名。连接方法是一边扫描一边配对,数据量大时连接性能通常优于子查询,但子查询的可读性更好。这道题的参考答案里还有第三种用exists的写法,属于相关子查询,放在后面的嵌套章节。面试时如果能随口说出三种写法,会显得对 SQL 执行逻辑理解得更透彻。
4.2 三表连接查课程名称条件:拿成绩、选课、课程三张表关联
第 3 题「查询数据库原理不及格的学生学号及成绩」需要三张表协作:
select sno, grade from sc, course where sc.cno = course.cno and cname = '数据库原理' and grade < 60;因为要通过课程名查成绩,必须先通过sc.cno = course.cno把选课表和课程表连接起来,才能过滤cname条件并取出grade。这里有个隐式内连接的陷阱:如果 sc 表里有某条记录对应课程在 course 表中不存在,也就是外键失效,内连接会把它丢掉。数据库不强制外键约束时,这种数据孤岛会被静默忽略。正规做法是给 sc 的 cno 加外键约束,或者审查数据质量。
第 4 题要连接三张表才能同时拿到学生姓名、课程名称和成绩:
select sname from student, sc, course where student.sno = sc.sno and sc.cno = course.cno and grade > 80 and cname = '数据库原理';嵌套写法:
select sname from student where sno in ( select sno from sc where grade > 80 and cno in ( select cno from course where cname = '数据库原理' ) );外层的连接版本一步到位,内层子查询版本层层剥洋葱。性能上连接通常更快,因为优化器可以更灵活地调整连接顺序和选择索引。子查询的好处是逻辑隔离,每一层只关心一件事。实际项目里如果数据量大,我会先把子查询写成临时表或者用with公共表表达式,让优化器有更好的执行计划。
4.3 left join 保留未匹配行:男学生没选课也不能漏
第 7 题「查询男学生学号、课程号、成绩,一门课程也没有选修的男学生也要列出」是所有连接题里最容易写错的:
select student.sno, sname, cno, grade from student left join sc on student.sno = sc.sno where ssex = '男';关键点是:left join以左表 student 为基准,右侧 sc 表没有匹配的行时,用 NULL 填充 cno 和 grade。如果一个男学生一门课都没选,sc 表里就没有他的行,只有left join才能把他保留下来。如果用 inner join 或等值连接,没选课的学生会被过滤掉。
这道题的隐藏陷阱在条件的位置:where ssex = '男'放在where里没问题,因为它只是过滤左表。但如果是left join sc on student.sno = sc.sno and ssex = '男',语义就变了——右表连接条件里的过滤会影响右表匹配行,但不影响左表保留。而把ssex = '男'放在where里,实际上是先过滤了左表再连接,结果等价。考试时会区分这两种写法的结果差异,比如把ssex='男'放进on后面,会导致左表不是「只保留男学生」,而是「男生参与连接,女生带 NULL 扩展」,结果集完全不同。
4.4 平均分不及格的连接版本与子查询版本
第 5 题「查询平均分不及格的学生的学号、平均分」需要连接 student 和 sc:
select student.sno, sname, avg(grade) from sc, student where student.sno = sc.sno group by student.sno, sname having avg(grade) < 60;这里group by后面必须同时包含student.sno和sname,因为select里两个非聚合列都要出现在group by中。如果只按sno分组而sname在 select 里,SQL Server 和 MySQL 的行为还不一样,MySQL 默认允许这种写法但取哪个姓名不确定,SQL Server 直接报错。养成group by写全所有非聚合列的习惯,跨数据库运行更安全。
嵌套子查询版本只需要在 sc 上分组,再回 student 表取姓名:
select sname from student where sno in ( select sno from sc group by sno having avg(grade) < 60 );第 6 题「查询女学生平均分高于 75 分的学生」把性别过滤和平均分过滤拆成两层:
select sname from student where ssex = '女' and sno in ( select sno from sc group by sno having avg(grade) > 75 );也可以写成连接版本:
select sname from sc, student where student.sno = sc.sno and ssex = '女' group by student.sno, sname having avg(grade) > 75;子查询版本先算平均分再过滤性别,连接版本先过滤性别再分组。数据量大时,先缩小 student 表再做连接往往更快;如果 sc 表很大,先用子查询把平均分大于 75 的学号集合算出来再 join,能减少连接时的笛卡尔积规模。这类优化思路在面试里经常被追问。
5. 嵌套查询与 EXISTS 相关子查询:避开三类常见陷阱
5.1 not in 与 not exists 的语义差异
第 2 题「查询没有选修 1002 课程的学生的学生」是一道必考题:
select sname from student where sno not in ( select sno from sc where cno = '1002' );用not in直观易懂。但如果 sc 表的 sno 列存在 NULL 值,这个查询结果就会变成空集。原因是 SQL 的三值逻辑:not in子查询返回结果集里包含 NULL 时,整体判断变成 UNKNOWN,所有行都被过滤掉。用not exists可以规避:
select sname from student where not exists ( select 1 from sc where sc.sno = student.sno and cno = '1002' );相关子查询not exists对每一行 student 执行一次内部查询,只要没有匹配到选课记录就保留该学生。性能上not exists通常优于not in,因为子查询命中即可短路,而且不受 NULL 影响。这道题的参考答案里贴了一个演示数据,sc 表里 0002 学生没有选 1002 课,所以只在结果中出现。
5.2 全称量词转换:没有一门课没选等价于选了所有课
第 4 题「查询没有选修 1001、1002 课程的学生」实际上有两层含义:是两门课都没选,还是至少有一门没选?题目的标准答案用的是双重否定逻辑:
select sname from student where not exists ( select 1 from course where cno in ('1001', '1002') and not exists ( select 1 from sc where sc.sno = student.sno and sc.cno = course.cno ) );内层not exists检查当前学生是否有某门课没选。如果学生对 1001 和 1002 课程都存在选课记录,内层not exists对所有课程都返回 FALSE,外层not exists就返回 TRUE,表示「不存在任何一门课该学生没选」,学生被选中。这是一个典型的全称量词(所有课都选了)转存在量词(不存在没选的课)的变换,SQL 没有直接的for all语法,只能通过双重否定实现。
这道题还可以用集合差集做:
select sno from student except select sno from sc where cno in ('1001', '1002');except返回左边集合有而右边集合没有的数据,语义比双重 NOT EXISTS 直观得多。但except要求两边的列数、类型完全兼容,而且性能和兼容性因数据库而异。Oracle 用minus,MySQL 的except支持取决于版本。如果考试没限制数据库方言,except是最优雅的解法。
5.3 极值查询的三种写法:top、all、相关子查询
第 5 题「查询 1002 课程第一名的学生学号」至少能写出三种方式:
-- 方法一:top select top 1 sno from sc where cno = '1002' order by grade desc; -- 方法二:all select sno from sc where cno = '1002' and grade >= all ( select grade from sc where cno = '1002' ); -- 方法三:相关子查询 select sno from sc x where cno = '1002' and not exists ( select 1 from sc y where y.cno = '1002' and y.grade > x.grade );top 1简单直接但只能查第一名,并列第一时只返回一行。>= all返回所有并列第一,不会漏人。相关子查询的not exists写法最通用,不依赖top或all关键字,任何数据库都能跑。如果 grade 有 NULL,>= all的行为会变得诡异:NULL 比较返回 UNKNOWN,>= all可能查不出任何行,需要用is not null先过滤。
第 6 题「查询平均分前三名的学生学号」把top和group by结合:
select top 3 sno from sc group by sno order by avg(grade) desc;这里order by avg(grade) desc才能按平均分排序取前三。如果平均分相同,top 3会任意返回三行,有可能漏掉并列。需要并列时改用窗口函数:
select sno from ( select sno, avg(grade) as avg_grade, dense_rank() over (order by avg(grade) desc) as rk from sc group by sno ) t where rk <= 3;窗口函数dense_rank会保留并列名次,比如两个第二名时,第三名也会被包含。SQL Server 2012 以上支持over子句,MySQL 8.0 也有。如果题目明确要求「前三名」,通常指的是dense_rank语义,只用top 3会被扣分。
关于相关子查询的经典考点——「查询大于本系科平均年龄的学生」,原题答案用别名 x 区分内外层:
select sname from student x where sage > ( select avg(sage) from student where sdept = x.sdept );外层每取一行学生 x,内层就计算一次该学生所在系的平均年龄,然后比较。这个查询的关键是内层where sdept = x.sdept,让内外层建立关联。没有这个关联,内层 avg 就是全校平均年龄,语义完全不同。相关子查询执行次数等于外层行数,数据量大时性能可能很差,通常可以用窗口函数avg(sage) over (partition by sdept)替代,一次扫描出结果:
select sname from ( select sname, sage, avg(sage) over (partition by sdept) as dept_avg from student ) t where sage > dept_avg;窗口函数在select阶段计算分区均值,不需要对每一行执行子查询,执行计划的 IO 成本更低。
6. 避坑与常见问题:刷这套题最容易踩的五个坑
6.1grade is null写成grade = null,整条查询白跑
现象:查询「1001 课程没有成绩的学生学号」返回空结果,但表里明明有空成绩记录。原因:= null永远返回 UNKNOWN,where子句只保留 TRUE 行。解决:所有判断空值的条件一律写成is null或is not null。顺带记住null参与的算术运算结果都是 null,比如grade + 1如果 grade 是 null,结果也是 null。
6.2where和having放错位置,聚合条件报错或者结果错
现象:写where avg(grade) > 80报「聚合函数不能出现在 WHERE 子句」。原因:where在分组之前执行,此时还没有聚合值。解决:对单行条件用where,对分组统计结果条件用having。我一般会先问自己「这个条件过滤的是原始行还是分组结果」,答案直接决定写在哪里。
6.3not in遇到子查询结果含 NULL,结果集莫名为空
现象:where sno not in (select sno from sc)返回空集,但数据明明有学生没选课。原因:子查询结果包含 NULL,not in比较时产生 UNKNOWN。解决:要么在子查询里加where sno is not null,要么直接改为not exists相关子查询。not exists没有这个坑,而且通常执行更快。
6.4top不带order by,取出的行没有确定顺序
现象:select top 1 sno from sc where cno = '1002'每次执行返回不同行。原因:top在没有order by时取的行是物理存储顺序,不确定。解决:极值查询必须配合order by,top 1 ... order by grade desc才能保证取到最高分那行。并列第一名时用top 1 with ties或者>= all,否则会漏数据。
6.5 连接查询里忘了加表别名导致列名歧义
现象:三表连接时where sno = '1001'报「列名不明确」。原因:sno在 student 和 sc 两个表都有,数据库不知道你指哪个。解决:连接查询里所有公共列都加表名前缀或别名。这道题还有个隐藏坑:用数字字符串查学号时,'1001'是长度为 4 的字符串,与学号列char(6)比较时数据库会自动补空格还是报类型不匹配,取决于字段类型定义。如果学号定义为char(6)而查询条件是'1001',某些数据库会补空格成'1001 ',匹配不到的记录就漏了。更稳的做法是把学号列定义为varchar(6),去掉尾部补空格的烦恼。
7. 查漏补缺的进阶验证:用结果集反向检查你的 SQL 是否正确
刷完这套题别急着对答案,先做一步验证。我自己的习惯是准备一份固定的小数据集,把每个查询跑出来的结果和手工推算的期望结果做对比,有差异就优先怀疑是条件边界或连接方向写错。比如「查询平均分前三名」这道题,先手工给 sc 表构造 5 个学生、每人 3 门课的成绩,平均分分别为 55、72、72、88、91,期望结果应该返回 88、91 和两个 72 的并列,共 4 行。如果top 3只返回了 3 行,说明并列没有处理。
验证外键类的题目,比如「查询男学生学号、课程号、成绩,没选课也要列出」,我会故意构造一个没有选课记录的男学生,确认结果里该学生出现在第一列有值、后两列为 NULL。如果查询结果里这个学生消失了,说明不小心用了 inner join 或等值连接。这个数据驱动的验证方式比看答案更靠谱,因为答案也可能有笔误,但你自己的数据预期不会说谎。
这份文档还提供了几个值得反复练习的变式。第 3 题「查询每门课程成绩都高于该门课程平均分的学生学号」是最难的相关子查询之一,参考答案写法是:
select sno from student where sno not in ( select sno from sc x where grade < ( select avg(grade) from sc where cno = x.cno ) );逻辑是全称量词转存在量词:不存在任何一门课,该学生成绩低于这门课的平均分。如果你把not in改成not exists,还能避免 NULL 成绩带来的空结果问题。这类题目做一遍可能不够,建议隔一周不看答案重做一次,确认逻辑链没有断层。
还有一点值得留意的是文档里没有但实际考试爱考的操作类考点——建表后加外键约束。原题只要求创建三张表并设定主键,但现实项目中 sc 表的外键必须指向 student 和 course:
alter table sc add constraint fk_sc_sno foreign key (sno) references student(sno); alter table sc add constraint fk_sc_cno foreign key (cno) references course(cno);加了外键后,删除 student 里被 sc 引用的行会被拒绝,必须先删 sc 里的参照行或设置级联删除。这个约束关系是数据库课程设计里必考的知识点,原题只到建表其实漏了这步。自己补上之后,整个 school 库的关系完整性才算闭环。
从那以后我每次拿到类似练习题,都会先建好数据再动手写 SQL,不靠答案反推题意。你可以先把这份文档跑通一遍,再按自己的数据结构改出三套变体题,那才是真正把 SQL 语法吃进去了。希望帮到你。
本文还有配套的精品资源,点击获取