资讯动态

student、sc、course三表SQL练习:从建表到多表查询的完整实践

发布时间:2026/10/9 7:48:01 来源:尧图企业网站定制
简介数据库系统概论课程的SQL练习表文档围绕学生表、选课表、课程表三张核心表展开面向正在学习数据库原理与SQL语法的高校学生帮助读者通过实际建表与插入数据掌握数据库的基本操作和完整性约束。整个资源包只有1个PDF文档约48KB内容包含创建数据库的命令、三张表的建表语句以及多组示例插入数据其中学生表以学号为主键并设置姓名唯一约束课程表通过预修课程号外键关联自身选课表则用学号和课程号组成复合主键同时外键关联学生表与课程表。学习者可以直接复制文档中的SQL语句到MySQL运行快速搭建一个学生选课练习环境随后针对单表查询、多表连接、成绩统计等典型题目进行实操并理解外键约束对插入顺序的影响。目前已有1907人学习下载适合作为数据库系统概论上机练习、期末复习或自学入门的轻量实用参考资料。1. 数据库系统概论SQL练习的经典三表student、sc、course刚学数据库系统概论的人十有八九都撞见过这套student、sc、course三表练习。它几乎是国内数据库课程里最通用的一套SQL练手数据student存学生基本信息course存课程信息sc存选课和成绩三张表靠主外键关联把关系模型最基本的一对多、多对多关系都装进去了。PDF里通常是一张表结构说明加几十道SQL题目从简单查询一路做到嵌套子查询。这东西能帮你解决的实际问题很直接把理论课上的关系代数、连接、聚合、分组这些概念落成一条条能跑的SQL顺手把考试题里那些套路摸熟。适合正在学数据库理论、准备期末考或面试前想快速找回SQL手感的人。别急着跳过——这套练习看着基础真正动手建库跑一遍翻车点比你想象的要多。2. 先把三张表建起来从PDF表结构到可运行的MySQL库2.1 读懂PDF里的表结构字段、主键、外键关系任何版本的student、sc、course练习表结构大同小异常见的字段定义如下。student表字段类型说明SnoCHAR(9)学号主键SnameVARCHAR(20)姓名SsexCHAR(2)性别SageSMALLINT年龄SdeptVARCHAR(20)所在系比如CS、IS、MAcourse表字段类型说明CnoCHAR(4)课程号主键CnameVARCHAR(40)课程名CpnoCHAR(4)先行课课程号可为空CcreditSMALLINT学分sc表字段类型说明SnoCHAR(9)学号联合主键一部分CnoCHAR(4)课程号联合主键一部分GradeDECIMAL(4,1)成绩可为空三张表的关系一定要先看明白再动手student和course相互独立sc通过Sno、Cno分别引用student和course形成两个一对多关系最终组合成多对多关系。换句话说sc表的主键是(Sno, Cno)这意味着同一个学生选同一门课只能有一条记录这是后面所有练习题的前提。很多题目问“没有选课的学生”“没人选的课程”本质都是在利用sc表的引用完整性做差集运算。2.2 建表DDL主键、外键、默认值一次写全我一般会按下面这套DDL在MySQL 8.0里建表。不要删掉外键约束练习阶段留着它反而能帮你发现数据错误。CREATE DATABASE IF NOT EXISTS school; USE school; CREATE TABLE student ( Sno CHAR(9) PRIMARY KEY, Sname VARCHAR(20) NOT NULL, Ssex CHAR(2) DEFAULT 男, Sage SMALLINT, Sdept VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( Cno CHAR(4) PRIMARY KEY, Cname VARCHAR(40) NOT NULL, Cpno CHAR(4), Ccredit SMALLINT, FOREIGN KEY (Cpno) REFERENCES course(Cno) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sc ( Sno CHAR(9), Cno CHAR(4), Grade DECIMAL(4,1), PRIMARY KEY (Sno, Cno), FOREIGN KEY (Sno) REFERENCES student(Sno), FOREIGN KEY (Cno) REFERENCES course(Cno) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明student表的Sname设为NOT NULL因为后续题目经常会按姓名做条件过滤空姓名会导致查询结果里出现不明不白的行。course表的Cpno是自引用外键指向course表自身的Cno这对应题目里那种“查询每一门课的间接先行课”的自连接需求。sc表的Grade用DECIMAL(4,1)能存0到999.9的成绩实际练习也用不到这么大的范围但两位小数和一位小数在排序、比较时表现不同统一用一位小数可以避开浮点比较的玄学问题。参数说明如果你用的是SQL Server或者PostgreSQL把CHAR(9)换成CHAR(9)或VARCHAR(9)都能跑通DECIMAL(4,1)也是标准语法。MySQL下建表建议显式指定ENGINEInnoDB保证外键约束生效——MyISAM引擎不检查外键插入脏数据不会报错练习时容易产生误导。utf8mb4是必须的否则中文系名、中文课程名在部分MySQL版本上会乱码。2.3 插入样例数据数据量要小但覆盖所有边界情况建完表就该灌数据。我会故意让数据覆盖几类边界情况有人没选课有课程没人选有成绩为空有同名同姓有先行课缺失。这样才能把后面题目里“查没选课的学生”“查成绩为空的学生”这类需求跑出有意义的返回值。INSERT INTO student (Sno, Sname, Ssex, Sage, Sdept) VALUES (201215121, 李勇, 男, 20, CS), (201215122, 刘晨, 女, 19, CS), (201215123, 王敏, 女, 18, MA), (201215124, 张立, 男, 19, IS), (201215125, 刘晨, 女, 20, CS), (201215126, 赵磊, 男, 21, MA); INSERT INTO course (Cno, Cname, Cpno, Ccredit) VALUES (1, 数据库, NULL, 4), (2, 数学, NULL, 2), (3, 信息系统, 1, 3), (4, 操作系统, 6, 3), (5, 数据结构, 7, 4), (6, 数据处理, NULL, 2), (7, PASCAL语言, 6, 4); INSERT INTO sc (Sno, Cno, Grade) VALUES (201215121, 1, 92.0), (201215121, 2, 85.0), (201215121, 3, 88.0), (201215122, 2, 90.0), (201215122, 3, 80.0), (201215123, 1, NULL), (201215124, 1, 70.0), (201215124, 2, 75.0), (201215124, 5, 60.0), (201215125, 1, 95.0);逻辑说明第5条和第2条学生同名“刘晨”这是故意造的为了让练习“查询所有姓刘的学生”这类题目时能看到结果集里出现多行避免你以为姓名唯一。course表里课程1的先行课为空课程3的先行课是课程1课程4的先行课是课程6但实际插入顺序是先插课程6再插课程4否则自引用外键会因为引用的行还没存在而报错。sc表里学生201215123选了课程1但成绩为NULL学生201215126完全没选课课程4、6、7暂时没人选——这几种情况是后面差集、空值判断题的题眼。参数说明插入顺序不是随意的。MySQL外键检查是逐行校验的插入course表时要保证被引用的Cno在表里已存在所以先插没有先行课的行再插有先行课的行。如果你不想费心排顺序可以在插入前执行SET FOREIGN_KEY_CHECKS0插完再SET FOREIGN_KEY_CHECKS1但建议练习时别这么做因为实际业务里关闭外键检查写数据是很危险的操作。3. 从单表到多表把PDF里的查询题拆成四层难度3.1 第一层单表查询练WHERE和ORDER BYPDF前面的题目基本是单表查询比如“查询全体学生的姓名、学号和所在系”“查询年龄在20岁以下的学生”。这类题不涉及连接主要练条件表达式的写法。SELECT Sname, Sno, Sdept FROM student WHERE Sage 20 ORDER BY Sno;逻辑说明WHERE后面是筛选条件ORDER BY在结果集上做排序。执行顺序上数据库先扫student表逐行判断Sage是否小于20满足条件的行进入结果集最后按Sno排序。这里有个新手常犯的错误——把列别名放在WHERE里用比如WHERE Sage 20想写成WHERE s_age 20但s_age这个别名在SELECT阶段才生成WHERE阶段根本看不到会直接报错。参数说明ORDER BY默认升序想倒序加DESC。如果排序字段上有索引数据库可能不走显式排序直接按索引顺序返回这在后面用EXPLAIN分析执行计划时会看到。练习时不用关心这个但要明白排序结果在不同数据库版本间可能不稳定——如果排序字段存在重复值MySQL不保证重复值之间的相对顺序需要加第二个排序字段消除随机性。3.2 第二层三表连接把多对多关系打通“查询每个学生的学号、姓名、选修的课程名及成绩”是这套练习最核心的题。它要同时用student、sc、course三张表因为学生姓名在student里课程名在course里成绩在sc里而sc正是连接两者的桥梁。SELECT student.Sno, student.Sname, course.Cname, sc.Grade FROM student JOIN sc ON student.Sno sc.Sno JOIN course ON sc.Cno course.Cno ORDER BY student.Sno, course.Cno;逻辑说明这个查询先让student和sc按Sno做等值连接得到每一行的学生信息和他们的选课记录再和course按Cno连接把课程号翻译成课程名。用的是INNER JOIN所以只返回在sc里有记录的学生——完全没选课的学生不会出现在结果里。如果题目要求“查询所有学生的选课情况包括没选课的学生”就必须把第一个JOIN改成LEFT JOIN写成FROM student LEFT JOIN sc ON ...这样没选课的学生会保留一行课程名和成绩显示为NULL。参数说明连接条件里的字段名如果带表名前缀就不容易产生歧义因为三张表里确实存在同名风险——student和sc都有Snosc和course都有Cno。这里的NULL值也有讲究如果学生的选课记录里Grade为空连接后该行Grade也是NULL排序时NULL会排在最前面还是最后面取决于数据库实现MySQL里NULL默认升序排最前实际练习看到和自己预期不符时先想到这一点。3.3 第三层聚合和分组统计类题的万能框架统计类题长这样“查询每门课的选课人数”“查询每个学生的平均成绩”“查询选修课程超过2门的学生学号”。它们统一用GROUP BY加聚合函数解决。SELECT sc.Cno, COUNT(*) AS cnt, AVG(sc.Grade) AS avg_grade FROM sc GROUP BY sc.Cno HAVING COUNT(*) 2 ORDER BY cnt DESC;逻辑说明GROUP BY sc.Cno把sc表按课程号分组每组是一门课的所有选课记录。COUNT()统计每组行数AVG只对非NULL的Grade求平均值——注意AVG会忽略NULL所以如果一门课有一个学生成绩为空分母是总选课数减一不是总选课数。HAVING是在分组之后过滤组和WHERE在分组之前过滤行完全是两个阶段。这里HAVING COUNT() 2筛掉只有一个人选的课结果只显示选课人数大于等于2的课程。参数说明COUNT()和COUNT(Grade)有本质区别。COUNT()数的是行数即使Grade为NULL也算COUNT(Grade)只数Grade不为NULL的行数。PDF里很多题的答案在这两个写法上是有讲究的——“查询每门课的考试人数”应该用COUNT(Grade)因为没成绩的不算考了“查询每门课的选课人数”用COUNT()更合理选了课就算。做练习时别图省事统一用COUNT()先明确题目问的是选课人次还是有效成绩人次。MySQL还有个默认坑SELECT子句里出现的非聚合列必须出现在GROUP BY里否则报错或随机取一个值。上面的写法SELECT了sc.Cno和聚合结果而GROUP BY正好是sc.Cno所以没问题。如果题目要求按系统计平均年龄写成SELECT Sdept, AVG(Sage) FROM student GROUP BY Sdept就成立。3.4 第四层子查询和自连接嵌套题的两种解法PDF后半段基本全是子查询题“查询成绩高于所有课程平均成绩的学生”“查询没有选任何课的学生”“查询每门课成绩高于该课程平均分的学生”。这类题的核心思路是先写一个内层查询得出一个集合外层查询再基于这个集合做判断。SELECT student.Sno, student.Sname FROM student WHERE NOT EXISTS ( SELECT 1 FROM sc WHERE sc.Sno student.Sno );逻辑说明这是典型的关联子查询。内层查询中sc表的Sno被student表的外层行绑定每扫描一行学生就查一次这个学生是否在sc里有记录。NOT EXISTS表示不存在任何一条选课记录所以返回的是没选任何课的学生。这里用EXISTS而不是IN是因为当子查询结果集包含NULL时IN的判断会变得不可靠——IN本质上做等值比较而NULL既不等于任何值也不等于NULL结果集里一旦混入NULLNOT IN会直接返回空集这套练习里sc表恰好有Grade为NULL的行能让你真实撞上这个坑。参数说明子查询里SELECT 1而不是SELECT *只是习惯写法EXISTS只关心是否有行返回不关心SELECT的列。有些教材喜欢用NOT IN 子查询写这道题写法是WHERE Sno NOT IN (SELECT Sno FROM sc)但前提是子查询的Sno列没有NULL——如果sc表的Sno列本身是主键一部分非空约束保证不会出现NULL但你自己建表时如果没加主键这里就可能翻车。所以练习时除非你完全确定子查询结果集不含NULL否则优先用NOT EXISTS。自连接也是这层难度里的高频题。“查询每一门课的间接先行课”课程表里课程3的先行课是课程1而课程1的先行课为空那么课程3的间接先行课还是课程1——准确说是查出每门课的先行课的先行课。SELECT c1.Cno, c1.Cname, c2.Cpno FROM course c1 JOIN course c2 ON c1.Cpno c2.Cno WHERE c2.Cpno IS NOT NULL;逻辑说明course表在这里被起了两个别名c1和c2本质上是把同一张表看成两张独立副本c1代表课程本身c2代表它的直接先行课。连接条件是c1.Cpno c2.Cno也就是把每门课的先行课信息接到这行上。WHERE c2.Cpno IS NOT NULL排除掉那些先行课本身没有先行课的课程。如果去掉这个条件结果里会出现一堆Cpno为NULL的行代表这些课程没有间接先行课。参数说明自连接不是MySQL专有SQL标准通用。它的特点是性能上要扫两次表但逻辑非常直观。如果课程表有上千行用自连接做这种题没问题如果上万行就要考虑用窗口函数或者递归CTE了但那是后话练习阶段先用自连接把语义搞明白。4. 更新和删除练习INSERT、UPDATE、DELETE的边界条件4.1 INSERT外键约束让插入选课记录不再自由PDF里更新的题量不大最常见的是“插入一条选课记录”和“修改某个学生的成绩”。这类题看表面简单实际上手全是约束问题。INSERT INTO sc (Sno, Cno, Grade) SELECT Sno, 1, 85.0 FROM student WHERE Sdept CS;逻辑说明这个插入不是硬编码一个学号而是从student表里动态查出所有CS系学生的学号给每人insert一条选了课程1的记录成绩85。这样做的好处是无论student表里CS系有多少人都能一次插完。坏处是如果有些CS系学生已经选过课程1会触发主键冲突——sc表主键是(Sno, Cno)同一个学生重复选同一门课就违反了主键唯一性整个INSERT语句全部回滚一条都插不进去。参数说明操作外部数据时如果你的PDF练习里给了具体学号和课程号直接INSERT INTO sc VALUES (201215121, 4, 90.0)就能跑通前提是201215121在student表里存在、课程4在course表里存在。如果某一边不存在外键约束直接报错。这是正常的——数据库在阻止你制造孤儿数据。我见过有人为了省事关掉外键检查插入之后再查数据发现对不上这就是拿练习数据养成了坏习惯。4.2 UPDATE先SELECT确认范围再UPDATE修改成绩的题比如“把所有学生的年龄增加1岁”或者“把某门课成绩低于60分的改成60分”注意这种题在MySQL Workbench里容易直接撞上安全模式。UPDATE sc SET Grade 60.0 WHERE Grade 60 AND Cno 5;逻辑说明UPDATE的执行逻辑是逐行扫描sc对满足WHERE条件的行执行SET赋值。这里把课程5低于60分的成绩改到60分。如果WHERE条件写错比如漏了AND Cno5就会把所有课程里低于60分的成绩全部改成60分而且这个操作无法撤销——除非你先做了备份或者提前用SELECT查一遍结果。参数说明MySQL Workbench默认开启了safe update mode语义是UPDATE或DELETE时WHERE条件必须用到索引列或主键否则拒绝执行。上面的查询条件只有Grade和Cno如果Cno上有索引就能跑没有索引就报错“You are using safe update mode”。常见处理办法是先执行SET SQL_SAFE_UPDATES0再跑UPDATE或者给WHERE补上主键范围条件。我一般推荐后者——练习归练习养成了关安全模式的手感到了生产环境早晚出事。4.3 DELETE删除父表数据前先想清楚子表怎么办“删除学号为201215125的学生记录”这类题难点不在DELETE语法本身而在于这个学生在sc表里有没有选课记录。如果有直接删student表会触发外键约束。DELETE FROM sc WHERE Sno 201215125; DELETE FROM student WHERE Sno 201215125;逻辑说明先删子表sc里的相关行再删父表student里的行顺序不能反。反过来的话MySQL会因为sc表里存在引用201215125的外键而拒绝删除。这个行为由建表时的外键定义的ON DELETE子句决定——如果你建表时写了ON DELETE CASCADE那么删除student时MySQL会自动删掉sc表里对应的行没写就报错。我建表时故意没写就是为了练习时能看到这个报错理解先删子表后删父表的顺序。参数说明有些练习题的PDF会要求“删除所有没选课的学生”这就要先用NOT EXISTS查出目标再删写法是把第3.4节那个查询改成DELETE FROM student WHERE NOT EXISTS (...)。执行之前强烈建议先把SELECT那段跑一遍确认要删的人数和预期一致再切换成DELETE。这是所有DML操作里最值得养成的一个习惯——先查后删给自己留后悔药。5. 三表练习避坑指南5个最常见的翻车现场5.1 三表连接丢条件结果多出大量重复行现象student、sc、course三表连完结果比预期多出几倍甚至几十倍的行数看起来每行数据都像重复了。原因连接条件漏写了一部分比如FROM student JOIN sc ON student.Sno sc.Sno JOIN course后面的JOIN course没写ONMySQL会把course表每一行都和前面结果做笛卡尔积行数暴涨。常见于漏了course的ON或者把student和course直接做连接没走sc。解决写多表连接时先数清楚ON的数量三张表需要两个ON。如果你在连接结果里看到某门课的课程名和成绩完全对不上先检查每个JOIN的ON字段名和关联方向再用COUNT(DISTINCT student.Sno)验证结果里的学生数是否和预期一致。5.2 分组查询SELECT了非聚合列MySQL直接报错现象执行GROUP BY查询MySQL报错“Expression #1 of SELECT list is not in GROUP BY clause”或者结果里某列的值看起来是随机取的。原因SQL标准要求SELECT里的非聚合列也必须出现在GROUP BY里。MySQL 5.7.5之后默认开启ONLY_FULL_GROUP_BY模式不再允许以前那种宽松写法。比如SELECT Sname, AVG(Grade) FROM sc JOIN student ON ... GROUP BY sc.CnoSname不属于分组列报错是正常的。解决要么把Sname加进GROUP BY要么改成只查询分组的维度列。如果确实想在分组结果里带上不属于分组条件的信息用子查询或者窗口函数解决不要靠关闭ONLY_FULL_GROUP_BY来硬跑——关掉的结果是同一组内随机取一个非聚合列的值这个“随机”在不同版本里表现还不一样很容易误导练习判断。5.3 LEFT JOIN的过滤条件写在WHERE里保住左表全量的目的落空现象写了LEFT JOIN想保留左表所有行最后结果里左表某些行还是消失了。原因过滤条件写错了位置。典型例子是FROM student LEFT JOIN sc ON student.Sno sc.Sno WHERE sc.Cno 1WHERE阶段会把左表里没选课程1的学生行过滤掉因为那一行的sc.Cno是NULLNULL不等于1。LEFT JOIN只保证JOIN阶段不丢行WHERE阶段会重新丢行。解决把过滤条件放在ON里写成LEFT JOIN sc ON student.Sno sc.Sno AND sc.Cno 1。这样左表所有行保留匹配不到课程1的学生显示NULL课程号和NULL成绩。记住一个判断标准只影响匹配结果的过滤放ON要影响最终输出行集合的过滤放WHERE。放在ON后的条件LEFT JOIN才会有保留左表全量的意义。5.4 IN子查询结果集里出现NULLNOT IN直接翻车现象用NOT IN子查询查没选课的学生返回结果为空但表里明明有没选课的人。原因子查询的Sno列没有非空约束或者查询结果里包含了NULL。NOT IN的语义是“不等于子查询结果里的任何一个值”而SQL里NULL参与比较结果既不是真也不是假是UNKNOWN。整个WHERE条件对每行都变成UNKNOWN过滤掉所有行。解决优先改用NOT EXISTS或者确保子查询结果集排除了NULL写法是WHERE Sno NOT IN (SELECT Sno FROM sc WHERE Sno IS NOT NULL)。练习中更推荐直接用NOT EXISTS因为它的语义就是“不存在”不受NULL干扰执行计划优化器处理EXISTS一般也更有优势。5.5 外键约束让DELETE、UPDATE寸步难行现象删除一条student记录报错“Cannot delete or update a parent row: a foreign key constraint fails”明明这条语句语法没错。原因sc表有外键引用student表被删除的学号在sc里存在选课记录数据库拒绝产生引用失效的数据。解决先查这个学生在sc里的选课记录DELETE FROM sc WHERE Sno ?再删除student表的记录。或者在建表时给外键加ON DELETE CASCADE但练习时不建议这么干因为会掩盖掉“先删子表再删父表”这个顺序意识。遇到这类报错不要违规关外键按正常业务顺序处理你会少踩很多隐形的数据一致性问题。6. 把练习当作评测工具验证SQL写对了的三个习惯很多人的练习方式是“写完SQL看结果不报错就觉得对了”但SQL不报错不等于答案对。我常用的验证方式是先手写预期结果再跑SQL把练习从“做题”变成“评测”。第一做任何统计查询前先通过简单查询估算答案的规模。比如“查询平均成绩大于80的学生”我会先跑SELECT COUNT(DISTINCT Sno) FROM sc确认总共有多少人选了课再跑完整查询对比返回人数是否在合理范围内。不在范围就直接看是不是WHERE和HAVING用错了——这是这套练习里最容易出问题的地方。第二用EXPLAIN看执行计划确认连接顺序和过滤下推是否合理。执行EXPLAIN SELECT ...看type列是否有index或ALLExtra列是否出现Using temporary或Using filesort。练习数据量小这些标记不会让你感觉到慢但出现在实际业务里就是性能炸弹。其中Using temporary常见于GROUP BY和DISTINCT混用Using filesort常见于ORDER BY没走索引遇到这两种标记就值得停下来想想能不能改写。第三给自己造一份“黄金答案集”。把每道题预期返回的关键行数或特征值记下来比如“查询所有学生及其选课信息”应该返回11行6个学生里5个有选课记录加上一个NA跑出来的行数对不上就说明JOIN或WHERE有问题。这是成本最低的自测方式因为这套练习的数据量小每道题的结果都能手工算出来手算的过程本身就是在巩固连接和分组语义。我自己的习惯是留一个专门建库的SQL脚本里面包含建表、插数据、以及我验证过的每道题的参考答案。碰到记不清的SQL语法直接翻脚本看当时的写法比翻PDF快得多。这套student、sc、course三表练习别看结构简单把它吃透之后你再看实际业务里那些动辄二十张表的复杂库至少不会对着JOIN条件心慌。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑