资讯详情 SQL刷题利器:student、sc、course三表建表与避坑指南
📅 2026/10/12 1:03:11
简介这是数据库系统概论课程配套的SQL练习表文档面向正在学习关系数据库基础与SQL语句的初学者也可作为期末上机复习的速查参考。文档以PDF形式呈现共1个文件、体积仅48KB内容聚焦student、course、sc三张核心表的建表与数据插入操作。其中student表包含学号、姓名、性别等字段并设置主键与唯一约束course表通过Cpno外键建立课程间的先修关系sc表则用复合主键关联学生与课程并记录成绩。针对course表受参照完整性限制无法一次性插入外键的情况文档给出了先插入课程再逐步更新Cpno与学分的操作方案同时提供了多组学生选课成绩示例数据便于读者直接复制运行。已有1907人学习使用适合配合数据库系统概论教材进行实践训练帮助理解主外键约束、唯一约束及数据插入顺序等关键概念。 如果《数据库系统概论》这门课的SQL练习只让你留一张纸那大概率就是student、sc、course三张表的题库学生、课程、选课成绩字段少到一眼看完却能串起单表查询、多表连接、分组聚合、子查询、集合操作这些从入门到进阶的SQL考点。期末复习、面试前恢复手感、验证自己是不是“看会了但一写就错”的人都能从这套表里找到对应的练习。但有个反直觉的事拿到这类PDF之后大多数人只做了“看题—记答案”两步真到写SQL时照样卡壳。问题通常不在语法而是没把三张表在本地建起来题干和字段始终停留在纸面。这篇笔记打算顺着student、sc、course把建库、造数、刷题、避坑的链路完整走一遍看完可以直接照着复现。2. 先把表结构吃透student、sc、course的字段设计、主键与外键选择拿到这类PDF练习第一步不是急着写查询而是把三张表的定义先落成数据库里的真实表。很多练习册把ER图、表结构、习题答案混排在一起眼睛看懂了手一写就漏约束。下面以最常见的教材版本为准student学生、course课程、sc选课。表结构其实有两种写法一种是全大写的SNO/SNAME一种是驼峰Sno/Sname以你手头PDF的为准别混用就行。2.1 三张表的字段清单sno、cno、grade为什么这样设计先把字段定义列出来后面所有刷题都基于这张表结构。表名字段类型约束含义studentsnoCHAR(9)主键学号studentsnameVARCHAR(20)NOT NULL姓名studentssexCHAR(2)默认男性别studentsageINT可空年龄studentsdeptVARCHAR(20)可空所在系coursecnoCHAR(4)主键课程号coursecnameVARCHAR(40)NOT NULL课程名coursecpnoCHAR(4)自引用外键先行课号courseccreditINT可空学分scsnoCHAR(9)联合主键/外键学号sccnoCHAR(4)联合主键/外键课程号scgradeDECIMAL(5,1)可空成绩sno选CHAR(9)而不是VARCHAR(9)是因为学号固定长度CHAR比较时不用算长度等值连接更快也不会因为数据里混入前后空格导致join失效。sname、sdept这类长度不固定的字段用VARCHAR省空间。grade用DECIMAL(5,1)表示最多三位整数加一位小数和教材里的百分制评分习惯对上。course表的cpno是自引用外键指向course.cno自己。课程“信息系统”的先行课是“数据库”所以它的cpno就填‘1’。“数据库”作为第一门课没有先行课cpno就是NULL。自引用在初期有点绕但它特别适合后面练习“查询每一门课的间接先行课”这类递归题。sc表是典型的关联表也叫桥表。一个学生选多门课一门课被多个学生选多对多关系在关系模型里必须拆成两个一对多sc就是中间的那张表。主键用(sno, cno)联合主键天然保证同一个学生不会重复登记同一门课。外键约束让数据库来保证引用完整性这是教材强调但初学者最容易忽略的部分。2.2 建表脚本一份能在MySQL 8直接执行的CREATE TABLE与INSERT下面这份脚本包含了建库、建表、造数MySQL 8里可以直接跑。为了能覆盖后面的子查询练习我在sc表里故意留了一行成绩为NULL的记录。-- 建库时直接指定字符集避免后面中文变乱码 CREATE DATABASE IF NOT EXISTS db_school DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE db_school; DROP TABLE IF EXISTS sc; DROP TABLE IF EXISTS course; DROP TABLE IF EXISTS student; -- 注意先删 sc再删 course最后删 student外键约束要求从子表开始删 CREATE TABLE student ( sno CHAR(9) PRIMARY KEY, sname VARCHAR(20) NOT NULL, ssex CHAR(2) DEFAULT 男, sage INT, sdept VARCHAR(20) ) ENGINEInnoDB; CREATE TABLE course ( cno CHAR(4) PRIMARY KEY, cname VARCHAR(40) NOT NULL, cpno CHAR(4), -- 先行课号自引用 ccredit INT, FOREIGN KEY (cpno) REFERENCES course(cno) ) ENGINEInnoDB; CREATE TABLE sc ( sno CHAR(9), cno CHAR(4), grade DECIMAL(5,1), PRIMARY KEY (sno, cno), FOREIGN KEY (sno) REFERENCES student(sno), FOREIGN KEY (cno) REFERENCES course(cno) ) ENGINEInnoDB; INSERT INTO student (sno, sname, ssex, sage, sdept) VALUES (201215121,李勇,男,20,CS), (201215122,刘晨,女,19,CS), (201215123,王敏,女,18,MA), (201215125,张立,男,19,IS); INSERT INTO course (cno, cname, cpno, ccredit) VALUES (1,数据库,NULL,4), (2,数学,NULL,2), (3,信息系统,1,4), (4,操作系统,3,3), (5,数据结构,NULL,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), -- NULL 表示还没有成绩很多练习题专门考这个 (201215125,1,95.0), (201215125,2,87.0);外键约束必须依赖InnoDB存储引擎MyISAM虽然在某些场景更快但根本不支持外键。练习平台上一会儿能建一会儿不能建多半就是引擎或者权限问题。生产环境里外键要不要用有争议但教材场景里建议开着它能帮你拦住不少脏数据。插入顺序有讲究先student再course最后sc因为sc的外键要引用前两张表的主键。反过来先插sc会直接违反外键约束。DROP TABLE的顺序正好相反先从有外键指向别人的子表开始删。建库时把默认字符集指到utf8mb4是性价比最高的一步。utf8mb4是utf8的超集能存emoji和生僻字MySQL 8的默认值也是它。很多人在线练习时中文乱码根源就是库和客户端字符集不一致。2.3 从PDF文本到可查数据库命令行source、Navicat与SQLite三种落地路径PDF里的建表语句通常是图片或者排版乱的文本最快的办法不是一行行在客户端里粘贴而是把字段定义统一整理成上面的school.sql文件再整体导入。三种常见路径我都用过。第一种是MySQL命令行直接导入。文件准备好后mysql -u root -p db_school school.sql是shell重定向把文件内容作为mysql客户端的输入。如果还没建库先执行CREATE DATABASE db_school;再跑这行。Windows下用记事本保存SQL文件时默认带BOM头有时会让第一个CREATE语句报错存成UTF-8无BOM更稳。第二种是图形工具导入。Navicat里连接数据库后右键选择“运行SQL文件”找到school.sql执行即可。工具的好处是能看到逐步执行的报错行号缺点是一旦文件编码不对报错信息会指向一个完全无关的位置排查起来很费劲。第三种适用于手边没有数据库服务的场景用Python内置的sqlite3import sqlite3 conn sqlite3.connect(school.db) with open(school.sql, r, encodingutf-8) as f: conn.executescript(f.read()) conn.commit() conn.close()SQLite对FOREIGN KEY默认不强制想在练习里验证外键行为执行前要加一句PRAGMA foreign_keys ON;。这个方案的好处是零安装缺点是对SQL语法的兼容度和MySQL有差异第5章会提到几个典型差异。3. 第一轮刷题用单表查询、去重、排序和三表连接练手表建好数据插完进入真正的刷题环节。这一轮题目看着基础但面试里翻车率最高的恰恰是这些条件写错、去重去错、连接连出笛卡尔积。我自己的经验是先把单表玩熟再碰连接和聚合顺序反了容易两头都学不扎实。3.1 单表查询与去重DISTINCT、WHERE、ORDER BY的正确姿势三道最基础的题覆盖单表过滤、去重、排序。-- 1. 查询所有系名去掉重复 SELECT DISTINCT sdept FROM student; -- 2. 查询1号课程成绩高于85分的学生学号和成绩按成绩降序 SELECT sno, grade FROM sc WHERE cno 1 AND grade 85 ORDER BY grade DESC; -- 3. 只要成绩前三名 SELECT sno, grade FROM sc WHERE cno 1 ORDER BY grade DESC LIMIT 3;DISTINCT是SQL里最容易理解错的语法之一。它作用于整行不是只作用于sdept一列。如果SELECT了sno和sdept两列再DISTINCT那去重是两列组合去重不是只按系去重。面试里有个高频追问“DISTINCT和GROUP BY都能去重区别在哪”先记住结论DISTINCT是查询完成后对结果集做去重GROUP BY是先分组再做聚合语义上GROUP BY能配合COUNT、SUM、AVG这些聚合函数DISTINCT不行。ORDER BY的DESC容易漏写。ASC是默认排序方向写不写都行。另一件容易忽略的事是NULL的排序位置MySQL里NULL默认排最前SQL Server默认排最后。跨数据库写练习时别拿这个顺序当标准答案。LIMIT是MySQL、SQLite的方言。SQL Server用TOPOracle用FETCH FIRST这就是第5章要单独说的环境差异。3.2 三表连接从student、sc、course查“选了数据库课的学生成绩单”多表连接是这套练习的核心三张表正好串起两个连接条件。-- 查询选了“数据库”课程的学生姓名、课程名、成绩 SELECT s.sname, c.cname, sc.grade FROM student s JOIN sc ON s.sno sc.sno JOIN course c ON sc.cno c.cno WHERE c.cname 数据库; -- 查询每门课的选课人数按人数降序 SELECT c.cname, COUNT(sc.sno) AS cnt FROM course c LEFT JOIN sc ON c.cno sc.cno GROUP BY c.cno, c.cname ORDER BY cnt DESC;三表连接本质上是两次两表连接student和sc先用学号对上得到每个学生的选课明细再把course加进来按课程号对上才能输出课程名。别名s、c、sc不是必需的但题目一多SQL会很长别名是给后续维护留余地的。第二段用了LEFT JOIN这是连接题里的分水岭。如果course表里有课程但没人选INNER JOIN会直接丢掉这行LEFT JOIN会保留课程并把COUNT计数为0。“查每门课的选课人数”和“查有学生选的课的选课人数”是两道不同的题区别就在连接类型上。下面这个查询能帮你确认连接有没有写对把student、sc、course三张表的sno、cno都打出来人工核对一遍JOIN的匹配关系。练熟了之后再碰到五表六表的连接也能拆成这种两两配对来看。3.3 分组聚合GROUP BY与HAVING的两个高频易错点-- 选课超过2门的学生学号 SELECT sno FROM sc GROUP BY sno HAVING COUNT(*) 2; -- 每个系学生的平均年龄只显示平均年龄小于20的系 SELECT sdept, AVG(sage) AS avg_age FROM student GROUP BY sdept HAVING AVG(sage) 20;HAVING不能被WHERE替代。WHERE是先过滤行再分组HAVING是先分组再过滤组。想查“年龄大于19的学生里每个系的人数”用WHERE先滤掉低龄学生再分组想查“平均年龄小于20的系”只能用HAVING因为AVG是分组之后才算出来的WHERE执行时这个值还不存在。另一个高频坑在SELECT列表。只按sno分组时SELECT里只能写sno和聚合函数。MySQL 5.7默认宽松模式允许写其他列比如直接写sname结果是从该组任意取一条靠运气MySQL 8默认开ONLY_FULL_GROUP_BY这样写直接报错。看到报错先别怀疑SQL写错确认一下环境的sql_mode。4. 第二轮刷题子查询、EXISTS与集合操作从会写变成会想基础题刷完真正的区分度在子查询和集合操作。很多数据库系统概论的练习册把这部分放在后半部分。我观察到的现象是能从IN改写成EXISTS、知道NOT IN会踩NULL坑的人SQL才算真正入了门。4.1 IN、NOT IN与EXISTSNULL是那个让你翻车的隐藏条件经典题目“查询没有选任何课程的学生姓名”。两种写法结果看似一样实际差别很大。-- NOT EXISTS 写法 SELECT sname FROM student s WHERE NOT EXISTS ( SELECT 1 FROM sc WHERE sc.sno s.sno ); -- NOT IN 写法 SELECT sname FROM student WHERE sno NOT IN (SELECT sno FROM sc);两张表数据完整时两条SQL结果一样。差别在语义上EXISTS是相关子查询对外层student每一行去sc里找有没有匹配记录IN是先把子查询结果集完整算出来再拿外层sno去比对。NOT IN一旦子查询结果里出现NULL整个查询会返回空结果。原因很简单NULL不参与等值比较x NOT IN (1, 2, NULL)的结果是UNKNOWN不会变成TRUE。而NOT EXISTS不存在这个问题它是逐行判断“不存在”遇到NULL也不受影响。面试题“IN和EXISTS有什么区别”考察的就是这个点。子查询里的SELECT 1是习惯写法只要存在行就满足EXISTS不需要返回具体列。写成SELECT *反而多传了列纯属浪费。4.2 UNION与UNION ALL合并选课名单时的重复行陷阱常见的集合操作题选1号课或2号课的学生有哪些。-- 自动去重 SELECT sno FROM sc WHERE cno 1 UNION SELECT sno FROM sc WHERE cno 2; -- 保留重复行 SELECT sno FROM sc WHERE cno 1 UNION ALL SELECT sno FROM sc WHERE cno 2;UNION默认去重代价是排序加比较两张大表union会明显变慢。如果能确定两个集合不可能相交直接UNION ALL它只是简单堆叠。这一点在慢sql优化里经常碰到属于收益很高的改写。另一个坑是ORDER BY的位置。整个UNION只能有一个ORDER BY放在最后一条SELECT之后作用于合并后的完整结果。不能在每条SELECT后面各写一个ORDER BY那是语法错误。4.3 把练习固化下来视图、索引与自建测试数据刷题刷到后半程建议把高频答案存成视图相当于给自己做一套“SQL答案快照”。CREATE VIEW v_student_score AS SELECT s.sname, c.cname, sc.grade FROM student s JOIN sc ON s.sno sc.sno JOIN course c ON sc.cno c.cno; SELECT * FROM v_student_score WHERE cname 数据库; -- 给外键列建索引 CREATE INDEX idx_sc_sno ON sc(sno);视图是固化了查询逻辑的虚拟表不占物理数据每次查它都会重新执行底层那条SQL。练习阶段用它保存常用连接查询比反复复制粘贴SQL更省事。视图能嵌套但别过度嵌套三层以上排查问题会很痛苦。索引要建在sc.sno和sc.cno上。因为sc是连接查询里的访问端执行JOIN sc ON sc.sno student.sno时如果sc.sno没有索引数据库就得全表扫描sc。练习表只有几行数据建不建索引体感没差别但用EXPLAIN能清楚看到type从ALL变成ref这是理解慢sql优化最直观的切入点。5. 练习表避坑实录建表失败、连接翻车、查询对不上的五个案例下面五条都是跑这套练习时真实踩过的坑每一条都按“现象→原因→解决”的顺序写。前两条属于环境问题中间两条属于SQL理解问题最后一条属于事务习惯问题。5.1 中文乱码与字符集现象INSERT中文后SELECT出来全是???或者建表直接报错Incorrect string value。原因数据库、表、列的字符集不是utf8mb4或者客户端连接字符集和服务端不一致。最常见的是建库时没指定字符集用了默认的latin1中文往里一写就崩。解决建库时带上CHARACTER SET utf8mb4连接数据库后先执行SET NAMES utf8mb4;再操作已经乱码的数据只能删除重建没有后悔药。字符集这个问题前期多花十秒指定后面少折腾一小时。5.2 外键不一致导致join翻车现象三表连接查出来的学生人数比student表里少但student表里明明有4个人。原因sc表里有student表不存在的sno也就是俗称的“孤儿数据”。这类情况在教材PDF里经常出现——练习数据是从不同章节摘录的前后行数对不上。解决先用下面这段SQL找出孤儿行再决定是补数据还是删数据。SELECT sc.sno FROM sc LEFT JOIN student s ON s.sno sc.sno WHERE s.sno IS NULL;如果结果为空说明外键关系没问题。如果查出有sno对照student表补上对应学生即可。这段“LEFT JOIN IS NULL”本身就是一道高频面试题用途是找“在A表但不在B表”的数据。5.3 连接查询查出笛卡尔积现象student和sc两张表连接结果行数等于8×432行明显不对。原因ON条件漏写或者JOIN写成了逗号连接却忘了WHERE关联条件。笛卡尔积是“看起来能跑但结果全错”的典型对新手有很强的迷惑性。解决写JOIN必须带ON哪怕是JOIN sc也要检查ON后面有没有写全。跑完查询先数行数连了几张表预期行数是主表的行数范围如果暴涨第一嫌疑就是连接条件。注意ON写反了也会产生类似问题比如把s.sno c.cno这种跨表错位条件写上去结果一样是垃圾。5.4 MySQL与SQL Server的语法差异现象同一段SQL在MySQL里跑得很正常换到SQL Server环境直接报错可能错在LIMIT也可能错在字符串拼接。原因SQL方言差异。很多院校期末上机用SQL Server网上的答案却多是MySQL写法。解决先确认练习PDF指定的是哪种数据库。下面这几个差异最常踩功能MySQL / SQLiteSQL Server限制返回行数LIMIT 3SELECT TOP 3 / OFFSET-FETCH字符串拼接CONCAT(sname, 的选修)sname 的选修标识符引用反引号方括号[ ]自增列AUTO_INCREMENTIDENTITY(1,1)SQL Server 2012以上支持OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY可以替代TOP实现更灵活的分页。如果只会在MySQL里写LIMIT到SQL Server上机前最好先把OFFSET-FETCH的写法过一遍。5.5 事务没提交数据“消失”了现象执行完INSERT换个窗口查却查不到或者数据库重启后数据不见了。原因连接开启了事务但没有执行COMMIT连接断开后事务自动回滚。还有一种是写入端事务未提交查询端在其他会话里当然看不到未提交的数据这不是丢数据是隔离级别在起作用。解决在命令行环境里执行写操作后主动提交或回滚。START TRANSACTION; INSERT INTO sc (sno, cno, grade) VALUES (201215125, 4, 90.0); -- 确认无误后提交 COMMIT; -- 发现写错了回滚 ROLLBACK;ROLLBACK就是SQL世界里的后悔药。练习阶段养成“写完看一眼再COMMIT”的习惯比事后补救省心得多。图形工具里默认自动提交反而容易掩盖这个问题。6. 用EXPLAIN和行数核对验收练习一个我自己常用的收尾技巧练习做到最后最怕的不是不会写而是写了不知道对不对。我现在的习惯是给每道题配一个“期望结果”清单先写清题目要求和期望行数跑完先看行数再看数据内容。比如“查询每个系的学生数”预期4行查出来5行一定是连接或分组出了问题。核对数据合法性也可以用一组快速校验SQLstudent表4行、course表7行、sc表8行外键孤儿数为0。这些数字跑一遍只要几秒但能把大部分环境问题挡在刷题之前。SELECT COUNT(*) FROM student; -- 期望 4 SELECT COUNT(*) FROM course; -- 期望 7 SELECT COUNT(*) FROM sc; -- 期望 8 SELECT COUNT(*) FROM sc s LEFT JOIN student st ON st.sno s.sno WHERE st.sno IS NULL; -- 期望 0要验证SQL本身的执行质量用EXPLAIN看执行计划EXPLAIN SELECT s.sname, c.cname, sc.grade FROM student s JOIN sc ON s.sno sc.sno JOIN course c ON sc.cno c.cno WHERE c.cname 数据库;EXPLAIN输出里的rows是预估扫描行数练习表只有几行看数值意义不大但能看三件事连接顺序、possible_keys有没有可用的索引、type列是ALL还是ref。把type为ALL的字段逐个补上索引再对比就能直观感受索引对连接查询的影响。进阶一点的验收方式是给练习SQL编号归档。每道题存成一个文件按题号命名ex01.sql、ex02.sql然后批量执行for f in exercise/ex*.sql; do echo $f mysql -u root -p db_school $f done这样每次改完表结构或插入新数据把整个目录重跑一遍等于给自己做了一次回归测试。哪道题结果变了一眼就能看到。带人做这套练习这几年我最深的感受是SQL不是靠看会的是靠把每一道题的输入、输出跑一遍才长进。哪怕只是把PDF里的题目抄成自己的SQL文件也比盯着答案看十遍强。这个习惯我一直保留到现在。希望帮到你。本文还有配套的精品资源点击获取