news 2026/10/14 10:15:19

《数据库系统概论》三表SQL实战:建表、查询与优化全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《数据库系统概论》三表SQL实战:建表、查询与优化全解析

简介:这份PDF文档面向正在学习《数据库系统概论》的高校学生与SQL初学者,围绕经典的student、sc、course三表模型提供可直接运行的建库建表与数据插入脚本,帮助读者在MySQL环境中快速搭建练习环境,解决课本示例难以复现、外键约束易报错等问题。资源共1个PDF文件,约48KB,内容以SQL语句与说明文字为主,涵盖创建sql_test数据库、定义三张表结构、设置主键唯一键与外键约束,以及分批插入学生、课程、选课成绩数据并更新预修课程号与学分的完整流程。其中course表的自参照外键与sc表的复合主键设计,能帮助读者理解参照完整性与规范化原则。目前已有1904人学习下载,适合配合教材做实验、完成课后习题或作为课程设计的基础数据模板,也可用于对照排查外键插入顺序等常见错误。

1. 三张表撑起一门课:这套 SQL 练习表到底能练什么

如果你正在学《数据库系统概论》,大概率绕不开 student、sc、course 这三张表。它们不是随便凑出来的示例数据,而是教材里用来讲透关系模型、参照完整性和多表查询的最小闭环。student 存学生基本信息,course 存课程信息且带先修课自引用外键,sc 存选课成绩并同时引用前两张表。三张表一摆,主键、外键、唯一约束、复合主键全齐了。这套练习表适合谁?刚学完 CREATE TABLE 语法但一写多表连接就卡壳的人,准备数据库原理考试需要反复刷查询题的人,以及想找个干净数据集练手 SQL 优化和窗口函数的人。它不依赖任何商业数据,导入就能跑,是少有的能让你把理论直接落到键盘上的练习素材。

2. 建库建表:从字符集到外键顺序的完整落地

2.1 建库前先定字符集,别等乱码了再后悔

很多人拿到建表语句直接复制执行,结果插入中文姓名时出现问号或乱码。问题不在 SQL 语句本身,而在创建数据库时没有显式指定字符集。MySQL 默认字符集在 5.7 和 8.0 之间有过变化,但最稳妥的做法是自己指定 utf8mb4,这样中文、生僻字甚至 emoji 都能正常存储。

-- 创建数据库时显式指定字符集和排序规则 CREATE DATABASE sql_test DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; -- 切换到这个库 USE sql_test;

逻辑说明:CHARACTER SET utf8mb4确保支持四字节字符,COLLATE utf8mb4_general_ci指定大小写不敏感的排序规则,适合学号、姓名这类字段的等值比较。如果你用的是 MySQL 8.0,默认排序规则可能是 utf8mb4_0900_ai_ci,功能上没问题,但显式写出来能避免换环境后行为不一致。

参数说明:sql_test是库名,你可以改成自己习惯的名字,但后续所有语句里的库名要同步改。字符集一旦建库后修改,已存在的表不会自动跟着变,需要单独 ALTER,所以建库时定好最省事。

2.2 三张表的创建顺序:先被引用的先建

外键约束决定了建表不能随心所欲。sc 表同时引用 student 和 course,所以 student 和 course 必须先存在。course 表还有一个自引用外键 Cpno 指向自己的 Cno,这个在单条 CREATE TABLE 里就能完成,不需要拆开。

-- 先建 student,被 sc 引用 CREATE TABLE `student` ( `Sno` char(20) NOT NULL, `Sname` char(20) DEFAULT NULL, `Ssex` char(2) DEFAULT NULL, `Sage` smallint DEFAULT NULL, `Sdept` char(20) DEFAULT NULL, PRIMARY KEY (`Sno`), UNIQUE KEY `Sname` (`Sname`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 再建 course,自引用外键 Cpno CREATE TABLE `course` ( `Cno` char(4) NOT NULL, `Cname` char(40) NOT NULL, `Cpno` char(4) DEFAULT NULL, `Ccredit` smallint DEFAULT NULL, PRIMARY KEY (`Cno`), KEY `Cpno` (`Cpno`), CONSTRAINT `course_ibfk_1` FOREIGN KEY (`Cpno`) REFERENCES `course` (`Cno`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 最后建 sc,同时引用 student 和 course CREATE TABLE `sc` ( `Sno` char(20) NOT NULL, `Cno` char(4) NOT NULL, `Grade` smallint DEFAULT NULL, PRIMARY KEY (`Sno`,`Cno`), KEY `Cno` (`Cno`), CONSTRAINT `sc_ibfk_1` FOREIGN KEY (`Sno`) REFERENCES `student` (`Sno`), CONSTRAINT `sc_ibfk_2` FOREIGN KEY (`Cno`) REFERENCES `course` (`Cno`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:student 的 Sno 是主键,Sname 加了唯一约束,意味着不能有两个同名学生。course 的 Cpno 外键指向自身 Cno,用来表示先修课关系,比如数据库的先修课是数据结构。sc 的主键是 Sno 和 Cno 的复合主键,保证一个学生同一门课只有一条选课记录。

参数说明:ENGINE=InnoDB是必须的,MyISAM 不支持外键。char(20)和char(4)是定长字符串,学号和课程号长度固定,用 char 比 varchar 查询略快。smallint存年龄和学分足够,成绩也是 smallint,范围 -32768 到 32767,完全覆盖 0 到 100。

注意:如果你在 MySQL 8.0 上执行,utf8mb4_general_ci可能提示过时,换成utf8mb4_0900_ai_ci即可,不影响表结构。

2.3 插入数据的顺序陷阱:course 表为什么要分两段写

原始资料里 course 表的插入分了两段:先只插 Cno 和 Cname,再用 UPDATE 补 Cpno 和 Ccredit。这不是多此一举,而是被自引用外键逼出来的。假设你想一次性插入('1','数据库','5',4),此时 Cno='5' 的记录还不存在,外键检查直接失败。所以必须先把所有课程号插进去,再回头更新先修课关系。

-- 第一段:只插课程号和课程名,Cpno 留空 INSERT INTO COURSE(Cno,Cname) VALUES('1','数据库'); INSERT INTO COURSE(Cno,Cname) VALUES('2','数学'); INSERT INTO COURSE(Cno,Cname) VALUES('3','信息系统'); INSERT INTO COURSE(Cno,Cname) VALUES('4','操作系统'); INSERT INTO COURSE(Cno,Cname) VALUES('5','数据结构'); INSERT INTO COURSE(Cno,Cname) VALUES('6','数据处理'); INSERT INTO COURSE(Cno,Cname) VALUES('7','PASCAL语言'); -- 第二段:更新先修课和学分 UPDATE COURSE SET Cpno='5', Ccredit=4 WHERE Cno='1'; UPDATE COURSE SET Cpno='4', Ccredit=2 WHERE Cno='2'; UPDATE COURSE SET Cpno='1', Ccredit=4 WHERE Cno='3'; UPDATE COURSE SET Cpno='6', Ccredit=3 WHERE Cno='4'; UPDATE COURSE SET Cpno='7', Ccredit=4 WHERE Cno='5'; UPDATE COURSE SET Cpno='5', Ccredit=2 WHERE Cno='6'; UPDATE COURSE SET Cpno='6', Ccredit=4 WHERE Cno='7';

逻辑说明:第一段插入时 Cpno 为 NULL,外键约束允许 NULL 值,所以不会报错。第二段 UPDATE 时所有 Cno 都已存在,外键检查通过。这个顺序不能颠倒,也不能合并成一条 INSERT。

参数说明:Ccredit是学分,数据库 4 学分、数学 2 学分,这些值来自教材示例。Cpno的取值必须在 Cno 集合内,否则外键报错。

student 和 sc 的插入没有顺序依赖,但 sc 的 Sno 和 Cno 必须分别存在于 student 和 course 中。原始数据里 sc 只插了 5 条记录,对应两个学生的选课,足够做基础查询练习。

-- student 插入 INSERT INTO `student` VALUES ('201215121', '李勇', '男', 20, 'CS'); INSERT INTO `student` VALUES ('201215122', '刘晨', '女', 19, 'CS'); INSERT INTO `student` VALUES ('201215123', '王敏', '女', 19, 'MA'); INSERT INTO `student` VALUES ('201215125', '张立', '男', 19, 'IS'); -- sc 插入 INSERT INTO SC(Sno,Cno,Grade) VALUES('201215121','1',92); INSERT INTO SC(Sno,Cno,Grade) VALUES('201215121','2',85); INSERT INTO SC(Sno,Cno,Grade) VALUES('201215121','3',88); INSERT INTO SC(Sno,Cno,Grade) VALUES('201215122','2',90); INSERT INTO SC(Sno,Cno,Grade) VALUES('201215122','3',80);

逻辑说明:student 的 Sname 有唯一约束,所以四个姓名不能重复。sc 的复合主键保证同一学生同一课程不会重复插入。Grade 允许 NULL,表示选了课但还没出成绩。

参数说明:学号用字符串存储,因为可能包含前导零或字母。性别用 char(2),存一个汉字刚好。系别 CS、MA、IS 分别对应计算机、数学、信息系。

3. 查询练习:从单表筛选到多表连接的递进路线

3.1 单表查询先把 WHERE 和 ORDER BY 用熟

建完表插完数据,第一轮练习应该聚焦单表。不要一上来就写三表连接,那样出错时你分不清是连接条件写错了还是筛选逻辑有问题。先从 student 表查所有男生,再按年龄降序排,最后加系别筛选。

-- 查所有男生,按年龄从大到小 SELECT Sno, Sname, Sage, Sdept FROM student WHERE Ssex = '男' ORDER BY Sage DESC; -- 查年龄在 19 到 20 之间的学生 SELECT Sname, Sage FROM student WHERE Sage BETWEEN 19 AND 20; -- 查姓刘的学生 SELECT Sno, Sname FROM student WHERE Sname LIKE '刘%';

逻辑说明:WHERE Ssex = '男'是等值筛选,ORDER BY Sage DESC让年龄大的排前面。BETWEEN 19 AND 20包含边界值。LIKE '刘%'中百分号匹配任意字符,如果只匹配一个字符用下划线_。

参数说明:字符串比较时 MySQL 默认不区分大小写,但中文没有大小写问题。如果 Ssex 存的是英文 M/F,需要确认排序规则是否区分大小写。

3.2 多表连接:INNER JOIN 和子查询各管什么场景

sc 表只有学号、课程号和成绩,要显示学生姓名和课程名就必须连接。INNER JOIN 适合需要同时展示多表字段的场景,子查询适合用另一张表的结果做筛选条件。

-- 查每个学生的选课情况,显示姓名、课程名、成绩 SELECT s.Sname, c.Cname, sc.Grade FROM sc INNER JOIN student s ON sc.Sno = s.Sno INNER JOIN course c ON sc.Cno = c.Cno ORDER BY s.Sname, c.Cname; -- 查选了数据库这门课的学生学号和姓名 SELECT Sno, Sname FROM student WHERE Sno IN ( SELECT sc.Sno FROM sc INNER JOIN course c ON sc.Cno = c.Cno WHERE c.Cname = '数据库' );

逻辑说明:第一个查询用两次 INNER JOIN 把三张表串起来,ON后面是连接条件。第二个查询用子查询先找出选了数据库的学生学号,再在外层筛选学生信息。两种写法结果一样,但执行计划不同,数据量大时 JOIN 通常更快。

参数说明:s和c是表别名,让 SQL 更短。ORDER BY s.Sname, c.Cname先按姓名排,姓名相同再按课程名排。

3.3 聚合与分组:统计每门课的平均分和选课人数

聚合函数是 SQL 练习的分水岭。COUNT、AVG、MAX、MIN 配合 GROUP BY 能回答很多实际问题,比如每门课有多少人选、平均分多少、最高分是谁。

-- 每门课的选课人数和平均分 SELECT c.Cname, COUNT(*) AS 选课人数, AVG(sc.Grade) AS 平均分 FROM sc INNER JOIN course c ON sc.Cno = c.Cno GROUP BY c.Cno, c.Cname HAVING COUNT(*) >= 1 ORDER BY 平均分 DESC; -- 查平均分高于 85 的课程 SELECT c.Cname, AVG(sc.Grade) AS 平均分 FROM sc INNER JOIN course c ON sc.Cno = c.Cno GROUP BY c.Cno, c.Cname HAVING AVG(sc.Grade) > 85;

逻辑说明:GROUP BY按课程分组,COUNT(*)统计每组行数,AVG(sc.Grade)算平均分。HAVING是对分组后的结果筛选,不能用 WHERE 替代。ORDER BY 平均分 DESC按别名排序,MySQL 支持这种写法。

参数说明:COUNT(*)统计所有行,包括 Grade 为 NULL 的记录。如果只想统计有成绩的,用COUNT(sc.Grade)。AVG会自动忽略 NULL 值,所以没成绩的记录不影响平均分计算。

注意:GROUP BY后面要列出所有非聚合字段,MySQL 5.7 之后默认开启 ONLY_FULL_GROUP_BY,不写全会报错。

4. 避坑与排查:外键、字符集和自引用的五个血泪教训

4.1 外键报错 1452:插入顺序错了还是被引用值不存在

现象:执行 INSERT 时报Cannot add or update a child row: a foreign key constraint fails。原因通常是 sc 表插入的 Sno 在 student 里没有,或者 Cno 在 course 里没有。解决:先查被引用表确认值存在,再插入。如果是批量导入,把外键检查临时关掉再开,但生产环境不建议。

-- 排查:先确认被引用值是否存在 SELECT Sno FROM student WHERE Sno = '201215121'; SELECT Cno FROM course WHERE Cno = '1'; -- 临时关闭外键检查(仅限批量导入场景) SET FOREIGN_KEY_CHECKS = 0; -- 执行插入 SET FOREIGN_KEY_CHECKS = 1;

4.2 自引用外键插入失败:course 表不能一次性写完

现象:INSERT INTO course VALUES('1','数据库','5',4)报外键错误。原因:Cpno='5' 的记录还不存在,自引用外键检查不通过。解决:按原始资料的两段式写法,先插 Cno 和 Cname,再 UPDATE 补 Cpno。

4.3 中文乱码:建库时没指定 utf8mb4

现象:插入中文姓名后 SELECT 出来是问号。原因:数据库默认字符集是 latin1 或 gbk,不支持部分中文字符。解决:建库时指定 utf8mb4,已建库的用 ALTER 修改。

-- 修改已存在数据库的字符集 ALTER DATABASE sql_test CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 修改表的字符集 ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

4.4 唯一约束冲突:Sname 重复插入报 1062

现象:插入两个同名学生的记录时报Duplicate entry。原因:student 表的 Sname 加了 UNIQUE KEY。解决:如果业务允许同名,去掉唯一约束;如果只是练习,改个名字或删掉旧记录。

-- 查看当前唯一约束 SHOW INDEX FROM student WHERE Non_unique = 0; -- 删除 Sname 的唯一约束(如果确认不需要) ALTER TABLE student DROP INDEX Sname;

4.5 GROUP BY 报错 1055:非聚合字段没写全

现象:SELECT Sname, AVG(Grade) FROM sc GROUP BY Sno报Expression #1 of SELECT list is not in GROUP BY clause。原因:Sname 不在 GROUP BY 里,MySQL 5.7 之后默认不允许。解决:把 Sname 加进 GROUP BY,或者用 ANY_VALUE(Sname) 绕过,但推荐写全。

-- 正确写法:GROUP BY 包含所有非聚合字段 SELECT s.Sname, AVG(sc.Grade) AS 平均分 FROM sc INNER JOIN student s ON sc.Sno = s.Sno GROUP BY s.Sno, s.Sname;

5. 进阶技巧:用窗口函数和 EXPLAIN 把练习表榨干

5.1 窗口函数排名:每门课成绩从高到低排

MySQL 8.0 支持窗口函数,这套练习表正好用来练 RANK、DENSE_RANK 和 ROW_NUMBER。比如给每门课的学生成绩排名,同一个课程内按成绩降序。

-- 每门课内按成绩排名 SELECT c.Cname, s.Sname, sc.Grade, RANK() OVER (PARTITION BY sc.Cno ORDER BY sc.Grade DESC) AS 排名 FROM sc INNER JOIN student s ON sc.Sno = s.Sno INNER JOIN course c ON sc.Cno = c.Cno ORDER BY c.Cname, 排名;

逻辑说明:PARTITION BY sc.Cno按课程分组,ORDER BY sc.Grade DESC组内按成绩降序,RANK()生成排名。成绩相同会并列,下一个名次跳号。如果不想跳号用DENSE_RANK(),想强制不并列用ROW_NUMBER()。

参数说明:窗口函数只能在 SELECT 列表和 ORDER BY 里用,不能写在 WHERE 里。如果只想看每门课第一名,把上面的查询包一层子查询再筛选排名 = 1。

5.2 EXPLAIN 看执行计划:连接字段有没有走索引

这套表的数据量很小,EXPLAIN 的输出可能看不出明显差异,但习惯要从小数据量养成。重点看 type 列和 key 列,type 是 ALL 表示全表扫描,ref 或 eq_ref 表示走了索引。

-- 查看三表连接的执行计划 EXPLAIN SELECT s.Sname, c.Cname, sc.Grade FROM sc INNER JOIN student s ON sc.Sno = s.Sno INNER JOIN course c ON sc.Cno = c.Cno WHERE s.Sdept = 'CS';

逻辑说明:EXPLAIN不实际执行查询,只返回优化器选择的执行计划。key列显示实际使用的索引,rows列是预估扫描行数。如果 sc 表的 key 是 PRIMARY,说明用了复合主键;如果 student 表的 key 是 PRIMARY,说明按 Sno 做了等值查找。

参数说明:WHERE s.Sdept = 'CS'没有索引,会全表扫描 student。如果经常按系别查,可以加索引CREATE INDEX idx_sdept ON student(Sdept)。但练习表数据少,加不加区别不大,知道原理就行。

5.3 用视图封装复杂查询,下次直接 SELECT

三表连接写多了容易烦,可以建一个视图把常用字段拼好,之后查视图就像查单表。

-- 创建视图:学生选课详情 CREATE VIEW v_student_course AS SELECT s.Sno, s.Sname, s.Sdept, c.Cno, c.Cname, c.Ccredit, sc.Grade FROM sc INNER JOIN student s ON sc.Sno = s.Sno INNER JOIN course c ON sc.Cno = c.Cno; -- 之后直接查视图 SELECT * FROM v_student_course WHERE Sdept = 'CS';

逻辑说明:视图不存数据,每次查询时动态展开。适合把频繁使用的连接逻辑固化下来,减少重复写 JOIN 的出错概率。视图也可以加 WHERE 条件,但性能不一定比直接写 JOIN 好,优化器会尝试合并。

参数说明:CREATE VIEW后面跟视图名,AS后面是 SELECT 语句。视图字段名默认继承 SELECT 的列名,也可以显式指定。删除视图用DROP VIEW v_student_course。

5.4 导入导出:把练习表备份成 SQL 文件

练完想换台机器继续,或者想分享给同学,用 mysqldump 把库导出成 SQL 文件最方便。

# 导出整个库的结构和数据 mysqldump -u root -p sql_test > sql_test_backup.sql # 只导出结构 mysqldump -u root -p --no-data sql_test > sql_test_schema.sql # 导入 mysql -u root -p sql_test < sql_test_backup.sql

逻辑说明:mysqldump是 MySQL 自带的命令行工具,导出的是 SQL 语句集合,包含 CREATE TABLE 和 INSERT。--no-data只导结构,适合做建表模板。导入时目标库要先存在,或者用CREATE DATABASE语句包含在导出文件里。

参数说明:-u root是用户名,-p提示输入密码。如果密码为空,直接回车。导出文件默认在当前目录,可以指定绝对路径。

从那以后我每次拿到新的练习库,都先跑一遍SHOW CREATE TABLE确认字符集和外键,再插数据,最后用 EXPLAIN 看一眼连接字段有没有走索引。这套习惯帮我省掉了至少三次返工。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 10:14:23

R ggplot双纵坐标轴原理与稳健绘制指南

1. 项目概述&#xff1a;为什么非得用双纵坐标轴&#xff1f;这事儿真不是炫技在R语言数据可视化实践中&#xff0c;“R ggplot绘制双纵坐标轴”这个需求&#xff0c;几乎每个做过跨量纲指标对比的从业者都踩过坑。我带过的几个数据分析项目里&#xff0c;某高校实验室做环境监…

作者头像 李华
网站建设 2026/10/14 10:13:31

多台PS5维护效率低?用AnyPS5工具搞定系统识别与升级包校验

如果你手头同时有两台以上PS5&#xff0c;而且不是同一批次收进来的&#xff0c;那我下面说的场景你多半经历过&#xff1a;每次系统一更新&#xff0c;最烦躁的事情不是下载那个动辄几十GB的升级包&#xff0c;而是你根本搞不清楚眼前这台机器到底处在哪个系统版本&#xff0c…

作者头像 李华
网站建设 2026/10/14 10:12:59

3 颗麦克风搞定 6 向声源定位:37×26mm 的 AR1105,接上就能用

智能小车要追着声音走&#xff0c;摇头摄像头要转向说话的人&#xff0c;会议设备要自动锁定发言人——这些功能的第一步都一样&#xff1a;先知道声音从哪个方向来。常规做法是上 4 麦甚至 6 麦的阵列&#xff0c;再配一套声源定位算法和 SDK&#xff0c;占体积、占算力、开发…

作者头像 李华
网站建设 2026/10/14 10:12:48

照着用就行:2026年最火AI论文软件榜单,AI工具一键写高质论文

2026 年实测 10 款主流 AI 论文工具&#xff0c;千笔AI以全流程覆盖 语义级降重 免费查重领跑综合榜&#xff1b;ThouPen 稳坐留学生毕业全流程工具头把交椅&#xff1b;免费工具中DeepSeek Scholar、豆包学术版表现亮眼&#xff0c;30 分钟即可生成万字高质量初稿&#xff0…

作者头像 李华
网站建设 2026/10/14 10:12:37

Agent技能系统设计实践:从协议定义到多技能编排

做Agent开发的朋友应该都有过这种体验&#xff1a;功能写了一堆&#xff0c;最后全挤在主流程里&#xff0c;改一个功能要翻半天代码&#xff0c;新加一个能力又得动核心逻辑&#xff0c;测试起来更是头疼。我早期做某智能助手项目时就掉进过这个坑&#xff0c;后来把能力全部技…

作者头像 李华
网站建设 2026/10/14 10:12:33

PDF处理避坑指南:用开源方案替代Nitro PDF的三大核心能力

简介&#xff1a;本资源为Nitro PDF真正破解版安装包&#xff0c;面向需要高效处理PDF文档的办公人员、学生及轻量级排版需求用户&#xff0c;解决PDF转Word、Excel、PPT等多格式转换难题&#xff0c;兼顾OCR基础识别与批量导出能力。压缩包为ZIP格式&#xff0c;大小41.96MB&a…

作者头像 李华