简介:数据库课后实验(崔巍编著)资源包为数据库课程配套的查询脚本合集,面向高校学生,适用于课后复习、实验参考与期末备考;资源共五个文件,全部为查询脚本,压缩包仅五千字节,轻巧易用。脚本贴合教材实验内容,从建表、插入、更新、删除等基础操作入手,逐步过渡到带过滤与排序的查询,再扩展至多表连接、子查询、分组与过滤等常用查询技巧。数据库设计方面,包含对第一、第二、第三范式的识别与分解练习,并借助实体联系模型转换案例理解关系模式规范化;事务处理部分通过开启事务、提交、回滚等语句演示原子性、一致性、隔离性和持久性,同时介绍并发控制与死锁概念,并涉及索引创建与查询优化相关实验。每条语句都可在数据库管理系统中直接运行,便于对照教材观察结果并修正错误;已有二百二十人学习下载,适合正在学习数据库课程、希望提升动手能力的同学使用。
1. 数据库课后实验到底在练什么:先跑通环境,再谈 SQL 手感
第一次打开这份数据库课后实验资源时,最常见的状态不是题目不会,而是环境没起来。我见过一些同学卡在第一步:装好了某个数据库软件,却不知道实验里要求的是哪个版本,也不知道哪些题需要写成脚本、哪些可以直接在命令行里跑出来。这套课后实验覆盖了从建库建表、增删改查、索引优化、事务处理,到存储过程与触发器的完整链路,本质是把数据库原理里的概念翻译成可运行的代码。适合第一次做数据库课程实验的初学者,也适合需要复查实验完整度的自学者。看完这篇文章,你能够照着顺序把环境搭起来,把每类实验的标准写法落地,并且提前避开那些最容易把结果带跑的坑。
2. 建库建表实验:环境选型、最小配置与可对照的落地脚本
数据库课后实验的第一章往往是建库建表,这部分的隐藏要求比表面上看起来要多。表面上是写几条CREATE TABLE,实际上还涉及到存储引擎选择、字符集统一、主外键关系是否被正确约束、列类型是否和后续的插入数据匹配。如果不先把这些前提定下来,后面做查询实验时会出现一批看似莫名其妙的结果差异。
2.1 实验环境选型:为什么多数课后实验默认 MySQL 8.x
数据库课后实验资源里出现的示例大多以 MySQL 为默认环境,原因不是只有 MySQL 能用,而是它覆盖了课后实验需要的所有功能点,同时安装和调试成本相对可控。MySQL 8.x 自带的默认字符集是utf8mb4,对中文支持比较完整,和实验里常见的“学生”“课程”“选课”这类中文字段名或中文数据兼容性更好。在动手之前,先确认本地是否已经有一个可用的 MySQL 8.x,我用一个最小检查命令来判断:
mysql --version如果输出版本号,说明客户端可用;接下来确认服务端是否在运行,再决定是直接登录还是先启动服务。
mysql -u root -p登录后建议先看几个关键参数:字符集、排序规则、事务隔离级别。这三个参数决定了后面实验里很多“看起来没错但结果不对”的问题到底出在哪。
SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'transaction_isolation';character_set_server默认值在 MySQL 8.x 里通常是utf8mb4,这能避免大量因字符集不一致导致的中文乱码。transaction_isolation默认是REPEATABLE-READ,做事务实验时要特别注意,因为这个隔离级别下,两个会话同时修改同一行数据的行为和READ-COMMITTED有明显区别。如果本机安装的是 5.7 版本,也不需要更换,只要在建库时显式指定字符集,后续实验同样能跑通。
2.2 建库建表脚本:从 ER 图到可执行 DDL
绝大多数课后实验的第一道题是建立一个“学生-课程-选课”关系模式。这里最合理的做法是先用一段 DDL 把整个数据库结构一次建完,而不是在命令行里一条一条地敲。下面这段脚本是一个可以直接对照着修改的模板:
CREATE DATABASE IF NOT EXISTS course_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; USE course_db; CREATE TABLE student ( sno CHAR(9) PRIMARY KEY, sname VARCHAR(20) NOT NULL, ssex CHAR(2) DEFAULT '男', sage SMALLINT, sdept VARCHAR(30) ) ENGINE=InnoDB; CREATE TABLE course ( cno CHAR(4) PRIMARY KEY, cname VARCHAR(40) NOT NULL, cpno CHAR(4), ccredit SMALLINT ) ENGINE=InnoDB; CREATE TABLE sc ( sno CHAR(9), cno CHAR(4), grade DECIMAL(5,2), PRIMARY KEY (sno, cno), CONSTRAINT fk_sc_sno FOREIGN KEY (sno) REFERENCES student(sno), CONSTRAINT fk_sc_cno FOREIGN KEY (cno) REFERENCES course(cno) ) ENGINE=InnoDB;这段 DDL 的逻辑很直接:先建数据库,再建三张表。student表里sno用CHAR(9)是因为学号通常是固定长度,使用VARCHAR反而会在比较时多一次类型转换。sage用SMALLINT而不是INT,能减少存储开销,也能避免某些实验环境里因为默认整数类型过宽导致的隐性类型问题。sc表用复合主键(sno, cno)保证同一个学生同一门课只能有一条记录。外键约束一定要显式加上,因为后续的插入异常实验会用到这些约束。
使用 InnoDB 引擎是这个场景下的推荐选择:它支持事务、外键和行级锁。如果实验题里不要求验证 MyISAM 的特性,就不要去换成 MyISAM,否则在事务实验里会出现“执行了回滚但数据还在”的误导结果。建完表后,可以用SHOW CREATE TABLE sc;检查外键是否有被正确记录,这一步能让你在开始插数据前就确认表结构完全符合预期。
2.3 数据初始化与三大注意事项
课后实验通常会给一批样本数据,学生表、课程表、选课表各若干行。初始化数据时要保持插入顺序:先插父表,再插子表。先插student和course,再插sc,否则外键约束会直接拒绝插入。下面这段插入脚本可以作为初始化的参考:
INSERT INTO student (sno, sname, ssex, sage, sdept) VALUES ('20180101', '张三', '男', 20, 'CS'), ('20180102', '李四', '女', 19, 'CS'), ('20180103', '王五', '男', 21, 'MA'); INSERT INTO course (cno, cname, cpno, ccredit) VALUES ('C001', '数据库', NULL, 4), ('C002', '数据结构', 'C001', 3), ('C003', '操作系统', 'C001', 3); INSERT INTO sc (sno, cno, grade) VALUES ('20180101', 'C001', 92.5), ('20180101', 'C002', 85.0), ('20180102', 'C001', 78.0);执行完插入后,用SELECT COUNT(*) FROM student;核对各表行数,是初始化阶段最稳妥的自检方式。这里有一个容易踩的细节:cpno列代表先修课程,它引用的还是course表自身的cno,严格来说应该加一个自引用外键,很多实验题的参考答案里并没有加,但这不影响大多数查询实验。如果后面的实验要求做“先修课程”的自连接查询,建议提前手动加上外键,这样写连接条件时会更明确。
3. 查询与索引实验:从零基础 SQL 到 EXPLAIN 执行计划
查询是数据库课后实验里题量最大的一章,表面考的是 SQL 语法,实际考的是对关系代数执行过程的理解。单表查询谁都能写两句,但真正拉开差距的是多表连接、子查询、分组过滤这三类写法的执行效率。做实验时不要只满足于“结果对”,每一步都要想清楚数据库到底是怎么把这条 SQL 跑出来的。
3.1 查询实验的层级:单表、连接、子查询与分组
课后实验里的查询题通常按难度递进排列。最开始是单表查询,比如查询年龄大于 20 的学生信息;接着是连接查询,比如查询选了“数据库”这门课的学生姓名;再往后是子查询和分组查询。以连接查询为例,最常见的标准写法如下:
SELECT s.sname, c.cname, sc.grade FROM sc JOIN student s ON sc.sno = s.sno JOIN course c ON sc.cno = c.cno WHERE c.cname = '数据库';这段查询先把sc表分别与student表、course表建立连接,再通过WHERE过滤出课程名为“数据库”的记录。关键点在于JOIN ON中的关联字段必须与表定义里的外键字段一致,这里sc.sno对应student.sno,sc.cno对应course.cno。如果外键约束已经建好,MySQL 会利用索引加速关联;如果没建外键,也没有在关联字段上建索引,两个连接操作就会退化为全表扫描。
分组查询是出现误解比较多的一类。看下面这个例子:
SELECT cno, AVG(grade) AS avg_grade FROM sc GROUP BY cno HAVING AVG(grade) > 80;WHERE在分组前过滤,HAVING在分组后过滤,两者不能互换。这个实验里常犯的错误是在WHERE里写聚合条件,结果数据库直接报错,提示你不知道avg_grade这一列。AS别名在HAVING里可以引用,在WHERE里不行,这是 SQL 执行顺序决定的。写分组查询时,建议每一条都先在脑子里过一遍“聚合发生在什么时候”。
3.2 索引什么时候该建:执行计划里的关键字段
索引实验是“数据库课后实验”里的重头戏。建索引不是越多越好,而是要在查询条件和连接字段上建。判断一个查询是否走了索引,看执行计划里几个关键列:type、key、rows。type从好到差大致是system、const、eq_ref、ref、range、index、ALL,见到ALL就意味着全表扫描。对一个连接查询来说,最理想的type是ref或eq_ref。下面这段操作先建立索引,再查看执行计划的变化:
CREATE INDEX idx_sc_cno ON sc(cno); EXPLAIN SELECT c.cname, sc.grade FROM sc JOIN course c ON sc.cno = c.cno WHERE sc.cno = 'C001';执行EXPLAIN后,查看key列是否变成idx_sc_cno,type是否从ALL变成ref,rows是否明显下降。rows表示 MySQL 估算需要扫描的行数,数值越大通常执行越慢。如果type还是ALL,可以先检查是不是sc表里数据量太少,优化器认为全表扫描比走索引更快,这是正常现象。想强制使用索引可以在表名后面加FORCE INDEX(idx_sc_cno),但实验报告里不建议这么写,更好的做法是把数据量增多后再对比。
3.3 用 EXPLAIN 验证索引是否命中
课后实验里经常要求“给某字段建索引,然后说明查询性能变化”。只贴一条CREATE INDEX命令是不够的,要用EXPLAIN把前后变化量化出来。比较规范的操作是先跑一遍没有索引的执行计划,记录type和rows,建索引后再跑一遍,记录同样两个字段。在 MySQL 8.0 以上版本,还可以用EXPLAIN ANALYZE查看实际耗时:
EXPLAIN ANALYZE SELECT s.sname, c.cname FROM sc JOIN student s ON sc.sno = s.sno JOIN course c ON sc.cno = c.cno WHERE sc.grade < 60;EXPLAIN ANALYZE会直接输出一个耗时说明,里面包含每个步骤实际处理的记录数以及花费的时间。注意这套语法对版本要求较高,如果实验环境是 MySQL 5.7,只能使用普通EXPLAIN。在校验时还要关注一个细节:索引不是建在 SELECT 后面的结果列上,而是建在WHERE和JOIN ON涉及的条件列上。课后实验里常有人给sname建索引,然后查询条件写的是sage,最终执行计划完全没有变化,这就是没分清索引的生效位置。
4. 事务与存储过程实验:把业务逻辑交给数据库的两个方向
事务、存储过程和触发器是数据库课后实验里最容易出现“结果对但理解不对”的部分。这三个功能点都在解决同一个问题:如何让数据库自己保证数据的一致性。事务负责把一组操作打包执行,存储过程负责把一段可复用的逻辑固化在数据库内部,触发器则负责在特定数据变化时自动执行动作。
4.1 事务实验:隔离级别、提交时机与回滚边界
课后实验里的事务题通常会设计一个转账场景,要求验证COMMIT与ROLLBACK的作用。最典型的做法是开两个终端会话,一个执行UPDATE不提交,另一个去查询同一行数据,观察是否看到未提交的修改。这个场景能否复现,取决于当前会话的隔离级别。下面这段是能在单个会话里演示事务回滚的最小脚本:
START TRANSACTION; UPDATE sc SET grade = grade + 5 WHERE sno = '20180101' AND cno = 'C001'; SELECT * FROM sc WHERE sno = '20180101' AND cno = 'C001'; ROLLBACK; SELECT * FROM sc WHERE sno = '20180101' AND cno = 'C001';第一次SELECT能看到成绩被修改,第二次SELECT在ROLLBACK之后看到的又是原来的值。这段代码验证的是“事务未提交前,修改对当前会话可见,但回滚后全部撤销”。注意一点:必须显式写START TRANSACTION,不能依赖命令行自动提交。大部分终端默认autocommit=1,没有START TRANSACTION时,每条UPDATE都会立即生效,ROLLBACK也就没有回滚对象了。检查当前自动提交状态可以用:
SHOW VARIABLES LIKE 'autocommit';如果输出是ON,建议在实验前显式执行SET autocommit=0;,避免漏掉事务边界导致回滚失效。做并发实验时,开两个终端后要记得分别设置隔离级别,并且确认两边查看的是同一个数据库和表,否则很容易误以为“锁失效了”。
4.2 存储过程实验:参数、游标与异常处理
存储过程的实验题一般要求写一个带输入参数和输出参数的过程,比如根据课程号统计选课人数。这里的关键不只是过程能跑通,还包括结束符的处理。MySQL 客户端默认把分号当作语句结束符,而过程体内部也有分号,所以要用DELIMITER先把结束符临时改掉。参考写法如下:
DELIMITER // CREATE PROCEDURE count_students_by_course( IN p_cno CHAR(4), OUT p_cnt INT ) BEGIN SELECT COUNT(*) INTO p_cnt FROM sc WHERE cno = p_cno; END // DELIMITER ; CALL count_students_by_course('C001', @result); SELECT @result;IN p_cno是传入参数,OUT p_cnt是输出参数。SELECT COUNT(*) INTO p_cnt是存储过程里常用的赋值方式,把查询结果直接写进输出变量。调用时先用@result这个用户变量接收,再用SELECT查看。存储过程实验里容易翻车的地方主要在DELIMITER:如果忘记改结束符,执行CREATE PROCEDURE时会把整个过程体拆成多段语句,导致创建失败。改完结束符后一定要恢复成默认分号,否则后续 SQL 语句全部会带上奇怪的解析规则。
4.3 触发器实验:审计日志与约束兜底
触发器是课后实验里比较难理解的一块,原因在于它是被动的:平时写 SQL 是“我让数据库做什么”,而触发器是“数据发生变化时数据库自动做什么”。实验题里最常见的需求是给选课表加一个审计触发器,把插入操作记录到日志表。核心代码如下:
CREATE TABLE sc_log ( log_id INT AUTO_INCREMENT PRIMARY KEY, sno CHAR(9), cno CHAR(4), op_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; DELIMITER // CREATE TRIGGER trg_sc_after_insert AFTER INSERT ON sc FOR EACH ROW BEGIN INSERT INTO sc_log (sno, cno) VALUES (NEW.sno, NEW.cno); END // DELIMITER ;触发器里的NEW代表插入后的新记录,NEW.sno和NEW.cno就是刚插入的数据值。如果想在删除时也记录日志,再写一个AFTER DELETE触发器,里面用OLD.sno和OLD.cno。做这个实验时最容易忽略权限问题:如果 MySQL 开启了二进制日志,创建触发器可能会报错,提示log_bin_trust_function_creators权限不足。此时需要执行SET GLOBAL log_bin_trust_function_creators = 1;授权,这个操作在部分数据库版本里需要管理员权限。触发器验证方式很简单,往sc表插入一条记录,再去sc_log表看是否多出一行,建议把验证步骤也写进实验报告,比口头解释更直观。
5. 数据库课后实验避坑区:五个把结果带跑的常见问题
数据库课后实验里的坑往往不是“不会写”,而是“看着写对了,结果一跑就偏”。下面这几条是我在实际复现这套实验时整理出的高频问题,每一条都按现象、原因、解决三个层次列出,可以当作排错清单来用。
5.1 中文乱码:现象、原因与复现排查
现象:向表里插入中文数据后,用SELECT查询出来是一串问号或者乱码。
原因:客户端连接字符集与服务器端字符集不一致。建库时指定了utf8mb4,但登录 MySQL 时没有指定连接字符集,或者连接的客户端编码还是latin1,中文字符在传输过程中就已经被截断。另一个常见原因是建表时没有指定DEFAULT CHARSET=utf8mb4,导致表沿用了数据库级别的旧字符集。
解决:先查看SHOW VARIABLES LIKE 'character_set_client';确认客户端字符集。在命令行或连接参数中显式加上连接字符集设置,常见做法是在登录后执行:
SET NAMES utf8mb4;如果使用的是图形化客户端,在连接编辑框里的编码一栏选择utf8mb4。建库建表时统一指定字符集和排序规则,不要混合使用utf8与utf8mb4,这两者虽然都能存中文,但排序规则不同,连接两个表时可能报字符集冲突的错误。
5.2 TRUNCATE 与事务回滚:改了表却救不回来
现象:在事务里执行TRUNCATE TABLE sc;,随后ROLLBACK,结果发现sc表的数据被清空,回滚没有生效。
原因:TRUNCATE属于 DDL 操作,DDL 会隐式提交事务。也就是说,即使前面写了START TRANSACTION,执行到TRUNCATE时事务已经终结,后面的ROLLBACK没有任何可回滚的内容。这和DELETE这类 DML 操作完全不同。
解决:实验里要求验证回滚时,清空表数据统一使用DELETE FROM sc;,不要使用TRUNCATE。如果题目明确要求比较两者区别,可以同时测试后,再在报告里说明“TRUNCATE 不支持事务回滚”这个结论。这个坑在课后实验里出现频率很高,不少人在写“删除表中的全部记录并要求可恢复”的题目时被拦下。
5.3 COUNT 的 NULL 陷阱:统计结果差一个
现象:对sc.grade做一个COUNT(grade),得到的结果比COUNT(*)少,但实验预期是两者相同。
原因:COUNT(column)只统计该列非 NULL 的记录数,而COUNT(*)统计表的全部记录数。只要有一条记录的grade是 NULL,两个结果就不可能一致。课后实验给的数据有时会有意留几个 NULL,专门验证考生是否理解这个语义差异。
解决:统计记录数时要明确实验要求的是“总行数”还是“某列非空数”。如果题目要求统计选课人数,标准写法是COUNT(*);如果题目要求统计有成绩的人数,才用COUNT(grade)。排查类似问题时,先执行SELECT * FROM sc WHERE grade IS NULL;看看是不是有 NULL 行在捣乱,再决定用哪个聚合函数。
5.4 触发器只能建不能跑:权限与日志模式问题
现象:CREATE TRIGGER执行成功,但往源表插入数据时,触发器不执行,sc_log表里没有新增记录。
原因:权限配置导致触发器创建成功但运行受限;或者触发器的表名和字段名写错,比如在触发器内引用了一张不存在的表,导致触发器执行时报错,但INSERT事务被吞掉,表面看不出来。还有一种偏隐蔽的原因是触发器名的前缀混淆,比如误建了BEFORE INSERT触发器,却在里面查询NEW.grade,此时数值还不是最终值,日志记录和预期不一致。
解决:先看触发器是否真的存在,执行SHOW TRIGGERS;检查状态。确认无误后,手动在触发器体内加一个SIGNAL语句做失败提示,这样触发器执行异常时能直接看到报错信息。如果确认是权限问题,按上一章的log_bin_trust_function_creators处理方案解决。做触发器实验时,建议把触发器和日志表放在同一张逻辑结构下,避免跨库引用带来的权限歧义。
5.5 外键删除顺序:删主表被拦住的完整流程
现象:删除student表里一条学生记录时,数据库报错,提示外键约束失败,无法删除。
原因:sc表里有外键引用这条学生记录。删除主表记录时,如果子表仍存在关联数据,数据库会拒绝执行,防止产生“孤儿数据”。这是关系完整性的正常保护,不是异常。
解决:按依赖顺序先删除子表关联记录,再删除主表记录。比较规范的实验操作如下:
DELETE FROM sc WHERE sno = '20180102'; DELETE FROM student WHERE sno = '20180102';如果实验设计里要求“删除学生时自动清理选课记录”,则需要给外键加上ON DELETE CASCADE,并在建表时指定。课后实验最常见的考核点是让考生判断删除顺序,所以不要省略子表清理这一步直接删主表。排查这类问题时,用SHOW CREATE TABLE sc;查看外键定义,能快速确认约束行为。
6. 交实验前的自检技巧:用元数据查询验证作业完整度
实验做完不等于可以交。课后实验资源里通常有十几个题目,最容易出现的情况是少建了一张索引、漏建了一个触发器、或者某张表里数据行数不对。手动一张表一张表地点开查看太慢,更高效的做法是直接用元数据查询把所有对象一次性列出来。下面这段脚本可以查出一个数据库里的全部表:
SELECT table_name, table_rows, engine FROM information_schema.tables WHERE table_schema = 'course_db' ORDER BY table_name;information_schema.tables里的table_rows对 InnoDB 表是估算值,不能当精确行数用。真正核对行数还是要用SELECT COUNT(*)。想检查外键和主键是否完整,可以查table_constraints:
SELECT table_name, constraint_type, constraint_name FROM information_schema.table_constraints WHERE constraint_schema = 'course_db';输出结果里能看到PRIMARY KEY、FOREIGN KEY、UNIQUE三类约束,逐一和实验要求对照,比打开建表语句一条条看更省时间。检查索引是否生效时,用SHOW INDEX FROM sc;查看Key_name和Cardinality。Cardinality数值很低,说明该列重复度太高,数据库优化器可能不会优先使用这个索引。
我个人的经验是,每次交实验前强制走一遍自检流程:先查表清单,再查约束清单,最后随机抽一条连接查询和一条事务操作原地验证。这个方法帮我避免过不少“打开文件才发现少建了一张索引”的情况。希望这套数据库课后实验的完整推进过程,能让你在做实验时少一点“玄学”,多一点把结果握在手心里的踏实感。
本文还有配套的精品资源,点击获取