简介:华中科技大学《数据库系统原理实践》课程实验资料,以MySQL为主线,适合正在修读该课程的学生,以及希望通过实验理解数据库内核的初学者。内容按实验模块组织,涵盖建库建表与完整性约束、数据查询、视图、触发器、存储过程与事务、并发控制与隔离级别、安全性、备份恢复、B+树索引的C++实现、数据库设计(含RBAC)及Java应用开发等完整环节。压缩包共92个文件,以62个SQL脚本为主,配有7个Java、6个C++源码、shell备份脚本、docx任务书、md说明、ER图和mwb模型等,整体仅1.42MB。文件按“实验-关卡”命名,便于快速定位;任务书、报告模板、评分细则等辅助文档齐全,可支撑从实验准备到报告撰写全过程。已有67人学习该资料,适合对照实验要求逐关练习并校对思路,是轻量且系统的MySQL实践参考。
1. 数据库系统原理实践:拿到这套MySQL资源后先想清楚的三件事
很多同学从某高校数据库系统原理课程拿到打包好的实践资源(以MySQL为例)之后,第一反应是解压、找实验文档、照着敲几条SQL,敲完关掉终端,回头一想还是什么都没记住。这套实践真正的价值不在于“完成几个查询”,而在于把关系代数、范式、事务隔离、索引这些抽象概念,落成一张张你能解释清楚的表,和一次次能稳定复现的现象。
它能解决三类具体问题:课程听完了但手生,实验报告只会贴代码说不出设计理由,以及面试被问到索引原理和隔离级别时只能背定义。适合正在上数据库原理课、需要以MySQL为引擎完成系列实验的本科生,也适合想用这套思路自检实操能力的开发者。动手之前先想清楚三件事:你的库里要有什么数据、实验串成什么主线、每一步靠什么标准判断对错。想清楚了再解压,效率会比直接翻实验文档高一倍。
2. 先跑通环境再看实验:MySQL版本选择与第一个可查询库
2.1 为什么课程实践以MySQL为例:选型理由
数据库系统原理课程可选的开源数据库不止一个,但MySQL几乎成了这类实践资源默认的载体,原因很实际。教材里的SQL标准语法在MySQL里基本都能直接跑,SELECT、JOIN、GROUP BY、子查询这些核心内容不会出现“教材写一套、数据库认另一套”的割裂感。其次,MySQL的安装包小、部署简单,一台普通笔记本就能跑起来,不像有些数据库动辄几个GB的安装体积,还要调一堆系统参数。
更重要的是,MySQL自带的information_schema、EXPLAIN、SHOW ENGINE INNODB STATUS这组工具,恰好能把课程里的原理变成可见的东西。索引有没有生效,EXPLAIN一行就能看到;死锁是怎么发生的,InnoDB状态里写得清清楚楚。这种“能被观察”的特性,对学习阶段的实践资源来说比性能还重要。相比之下,一些商用数据库的功能更强,但授权和安装门槛不适合课堂环境。实践资源选MySQL,本质上是选了一条从原理到现象最短的验证路径。
2.2 环境准备三步:确认版本、连接客户端、建初始库
拿到资源包后不要急着看实验内容,先把MySQL环境对齐。我一般建议统一装8.0版本,原因有三个:默认字符集就是utf8mb4,省去一堆乱码问题;CHECK约束从8.0.16开始真正执行,做完整性实验时行为更符合教材;窗口函数也能用,做进阶查询不用绕路。先确认版本和登录方式:
mysql --version mysql -u root -p-u root指定用户,-p表示登录时需要输入密码,密码不会显示在终端上。如果你在某个机器上装的是5.7,也不用急着升级,后面章节里我会专门讲5.7和8.0在行为上的几个关键差异,实验前心里有数就行。
登录进来之后,建一个独立的实验库,不要把建表脚本直接丢进root用户默认的测试库里。课程实践一般会围绕一个教学场景建多张表,独立库能避免后续做DROP、TRUNCATE实验时误伤其他数据。
CREATE DATABASE school DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;DEFAULT CHARACTER SET utf8mb4指定库的默认字符集,utf8mb4是完整UTF-8实现,能存中文和表情符号;COLLATE utf8mb4_0900_ai_ci指定排序规则,ai表示口音不敏感,ci表示大小写不敏感,这是8.0的默认组合。建完库后用SHOW DATABASES;确认school在里面,环境就算通了一半。
2.3 导入一份能跑的初始数据:source命令与三条验证SQL
环境通了一半,另一半是让库里真的有数据。课程实践资源里通常会带建表脚本和示例数据,但也经常出现脚本路径不对、编码不对、导到一半报错的情况。我习惯不看资源包里那个“一键导入”说明,而是自己先建一个最小数据集,确认链路通畅,再决定用不用资源包自带的脚本。下面这三张表是数据库原理实验最常见的主线:学生、课程、选课。
CREATE TABLE student ( student_id INT NOT NULL, name VARCHAR(20) NOT NULL, gender CHAR(1), enroll_year YEAR, PRIMARY KEY (student_id) ) ENGINE=InnoDB; CREATE TABLE course ( course_id INT NOT NULL, title VARCHAR(50) NOT NULL, credits DECIMAL(3,1), PRIMARY KEY (course_id) ) ENGINE=InnoDB; CREATE TABLE sc ( student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2), PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINE=InnoDB;这段脚本里有几个关键设计:性别用CHAR(1)而不是VARCHAR,定长字段检索更快;学分用DECIMAL(3,1),因为浮点数存金额和成绩会有精度误差,DECIMAL是精确类型;选课表的主键是(student_id, course_id)联合主键,天然保证同一个学生同一门课只有一条记录。ENGINE=InnoDB必须写,外键约束和事务都依赖它。
把上面的脚本保存成schema.sql,然后在mysql客户端里执行导入:
mysql -u root -p school < schema.sql这条命令把当前目录下的schema.sql定向导入school库。导入成功后,用三条SQL验证库是真的能用,而不是只建了空表:
SHOW TABLES; SELECT COUNT(*) FROM student; SELECT s.name, c.title, sc.score FROM sc JOIN student s ON sc.student_id = s.student_id JOIN course c ON sc.course_id = c.course_id ORDER BY sc.score DESC;JOIN语句能跑通,说明外键关系和数据类型都没问题。走到这一步,你的环境才算真正准备好——后续所有实验都是在这个基础上叠加的。很多人的实践翻车不是翻在实验本身,而是环境没对齐就开始跑,最后查不出来是数据问题还是配置问题。
3. 从ER模型到SQL落地:建表、数据操纵与索引优化的完整练习路径
3.1 把概念模型变成关系模式:主键、外键与CHECK的真实约束
数据库系统原理课程的前半段核心是ER模型转关系模式,这部分在纸面上做很容易,但落到MySQL里会暴露一堆教材没写清楚的细节。转换规则并不复杂:实体变表,属性变字段,一对多关系在“多”端加外键,多对多关系单独建一张中间表。关键是字段类型和约束的选择,这决定了表能不能真实反映业务规则。
最常见的翻车点是可选性设计。比如学生实体里的“性别”字段,教材里写“取值范围为男或女”,但如果你只把gender CHAR(1)写在建表语句里,MySQL 8.0.16之前的版本根本不会校验取值,插入'X'也不会报错。正确的做法是显式加上CHECK约束:
CREATE TABLE student ( student_id INT NOT NULL, name VARCHAR(20) NOT NULL, gender CHAR(1), enroll_year YEAR, PRIMARY KEY (student_id), CONSTRAINT chk_gender CHECK (gender IN ('M', 'F')) ) ENGINE=InnoDB;CONSTRAINT chk_gender给约束起了名字,后续要删除或修改时能直接引用。CHECK (gender IN ('M', 'F'))在8.0.16以上版本会真正拦截非法值。这里要注意:5.7及更早的版本会解析CHECK语法但不执行,很多同学的实验报告里写了CHECK却说“没生效”,多半是版本问题。
外键的设计也要想清楚。选课表的两个外键默认行为是RESTRICT,也就是说,如果某个学生已经选了课,直接DELETE student表里对应行会被拒绝。这是合理的默认值,但做实验时要心里有数——教材里讲的“级联删除”不是默认行为,需要显式写ON DELETE CASCADE。建议实验时两种都试一遍,观察报错信息和数据变化,这比背外键约束级别定义有用得多。
3.2 数据操纵练习:INSERT/UPDATE/DELETE的边界行为
实验资源里一般会要求做一批数据操纵练习,很多同学把INSERT、UPDATE、DELETE当成“填数据”就过去了,其实这部分是理解约束最好的素材。别只写正确的语句,故意写几条会失败的,观察MySQL报什么错。比如插入重复主键:
INSERT INTO student (student_id, name, gender, enroll_year) VALUES (1, '张三', 'M', 2023); INSERT INTO student (student_id, name, gender, enroll_year) VALUES (1, '李四', 'M', 2023);第二条会报Duplicate entry '1' for key 'student.PRIMARY',这行错误信息直接对应教材里“实体完整性”的概念。同样值得试的是违反外键约束的插入:
INSERT INTO sc (student_id, course_id, score) VALUES (999, 1, 88);如果student表里没有999这个学生,MySQL会报Cannot add or update a child row: a foreign key constraint fails。这条错误信息是理解“参照完整性”的最佳入口,比看十遍教材定义都深刻。
UPDATE和DELETE的边界更容易被忽略。比如在没有WHERE条件的情况下执行DELETE FROM course;,会直接清空整张表,外键引用它的sc表也会因为约束冲突而拒绝删除。实践里有一种危险操作是把WHERE条件写错,比如更新学生姓名时忘了加引号,或者用score = score + 5时误更新了所有行。我的习惯是每次UPDATE和DELETE之前先用相同WHERE条件的SELECT查一遍,确认影响范围再执行。这个习惯在实验报告里也能体现出来——写清楚你预期影响几行、实际影响几行,比只贴一条成功的SQL更有说服力。
3.3 用EXPLAIN验证索引:这条慢查询到底慢在哪
索引实验是数据库系统原理实践里最能体现“原理”的部分,也是面试常考点。不做索引时,MySQL执行查询只能全表扫描,数据量小感觉不到,但EXPLAIN的结果会直接告诉你真相。先用一个没有索引的查询练手:
EXPLAIN SELECT * FROM sc WHERE student_id = 42;EXPLAIN的输出里重点看三列:type如果是ALL,就是全表扫描,意味着MySQL把sc表的每一行都读了一遍;key如果是NULL,说明这条查询没用到任何索引;rows是MySQL估算扫描的行数,全表扫描时等于表的总行数。
然后给sc表的student_id字段加上索引:
CREATE INDEX idx_sc_student ON sc(student_id);再跑一次EXPLAIN,type会从ALL变成ref,key变成idx_sc_student,rows明显下降。ref表示通过索引精确定位到匹配的行,不再扫描全表。这一步的变化,就是教材里“索引加快检索速度”的直接证据。
更进阶一点的练习是覆盖索引。如果查询只需要student_id和score两个字段,可以把索引建成(student_id, score)的联合索引,EXPLAIN的Extra列会出现Using index,意思是查询所需数据全部在索引里,连回表都省了。实践资源如果给了索引相关的选做实验,建议往这个方向做深一点。注意联合索引有最左前缀原则,索引(student_id, score)对WHERE student_id = ?有效,但单独用WHERE score = ?时用不上这个索引。这个坑在实验里很容易踩,踩过一次就记住了。
4. 事务与隔离级别实验:用两个终端把脏读和幻读复现出来
4.1 四个隔离级别先定位:SESSION级设置命令与查看当前级别
事务与隔离级别是数据库系统原理实践里最抽象的一块,很多同学背得下四个隔离级别的定义,却从没见过脏读和幻读长什么样。原因很简单:教材里的例子是并发场景,单终端单会话根本复现不出来。做这个实验至少要开两个mysql终端,一个会话跑事务,另一个会话观察数据变化。
先确认当前会话的隔离级别:
SELECT @@transaction_isolation;这条语句在MySQL 8.0返回REPEATABLE-READ,也就是默认隔离级别。想切换隔离级别,用SET SESSION而不是SET GLOBAL,避免影响其他连接:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;SESSION表示只对当前会话生效,实验结果不会污染后续实验。每次切换后,重新执行SELECT @@transaction_isolation;确认切换成功。四个级别里,面实验最常用的是前三个,SERIALIZABLE几乎很少遇到,知道它的效果就行。
4.2 两个终端的复现步骤:脏读、不可重复读与幻读的观察点
假设你已经导入了第2章的student表,并且里面有一条student_id=1, name='张三'的记录。终端A负责写,终端B负责读,两个终端都连同一个school库。终端B先把隔离级别改成READ UNCOMMITTED,然后开始事务并查询:
-- 终端B SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; SELECT * FROM student WHERE student_id = 1;这时候终端A不提交事务,直接更新这条记录但不COMMIT:
-- 终端A START TRANSACTION; UPDATE student SET name = '李四' WHERE student_id = 1;回到终端B再查一次:
-- 终端B SELECT * FROM student WHERE student_id = 1;如果看到name已经变成李四,脏读就复现了——你读到了别人还没提交的数据。这是READ UNCOMMITTED级别的特性,也是教材定义的直接验证。终端B执行ROLLBACK;结束这次观察。
不可重复读的实验要把终端B切到READ COMMITTED级别,因为MySQL默认的REPEATABLE READ在这个场景下显示不出不可重复读。切换到READ COMMITTED后,终端B开启事务先查一次,终端A更新并COMMIT,终端B再查一次,两次结果不同,这就是不可重复读。幻读的实验更讲究:MySQL的REPEATABLE READ用了间隙锁,常规的UPDATE之后INSERT新行、再查询多了一行的操作,在InnoDB下未必能复现出幻读,这是实践里最容易让人怀疑人生的地方。我一般的验证方法是直接观察锁行为:终端A在REPEATABLE READ下对WHERE student_id BETWEEN 1 AND 10加锁并保持事务不结束,终端B试图插入student_id=5的新记录,B会被阻塞住。这个阻塞现象比“多读了一行”更能说明间隙锁的存在。
4.3 锁等待与死锁的观察命令与处理习惯
做并发实验时难免把事务开多了不关,导致后续操作全部卡住。这时候别急着重启MySQL,先看当前有哪些事务在跑:
SELECT * FROM information_schema.INNODB_TRX\G这个视图会列出所有正在执行的事务,包括事务的持续时间、状态、SQL语句片段。如果发现某个事务长时间处于RUNNING状态,而且大量其他操作被阻塞,大概率就是它没COMMIT也没ROLLBACK,持有的锁一直没释放。
死锁的现场更直观。MySQL检测到死锁后,会自动回滚其中一个事务,让另一个继续,终端里会报Deadlock found when trying to get lock; try restarting transaction。想看死锁的详细过程,用InnoDB的状态输出:
SHOW ENGINE INNODB STATUS\G重点看LATEST DETECTED DEADLOCK段,里面会记录死锁发生时两个事务各持有哪些锁、在等哪个锁。这个输出比任何教材都真实。实验报告里如果能贴出这段日志并解释两个事务的锁竞争关系,含金量会明显不一样。我以前做实验时经常把事务开着忘了关,后续实验全部卡死,还以为是数据库坏了,后来养成习惯:开事务前先看一眼INNODB_TRX,实验结束立刻COMMIT或ROLLBACK,这个习惯救了我很多次。
5. 数据库系统原理实践避坑:5个常见翻车点与排查方法
5.1 建库没指定字符集,导入中文全变问号
现象:按照资源包的说明导入SQL脚本,建好的表里中文全部显示为问号,或者插入中文直接报Incorrect string value错误。原因:MySQL的默认字符集在某些配置下仍是latin1,不支持中文字符。解决:建库时显式指定字符集,如CREATE DATABASE school DEFAULT CHARACTER SET utf8mb4;如果库已经建了,用ALTER DATABASE school CHARACTER SET utf8mb4;修改,同时把表的字符集也改掉。导入脚本时还可以在连接参数里加--default-character-set=utf8mb4,确保客户端和服务器字符集一致。这个坑在Windows环境下尤其常见,因为部分安装器默认初始化的字符集不是utf8mb4。
5.2 MySQL 8.0 认证插件导致老客户端连不上
现象:用某个旧版本的可视化客户端连接MySQL 8.0,报Authentication plugin 'caching_sha2_password' cannot be loaded,但用命令行mysql客户端又能正常登录。原因:MySQL 8.0默认的认证插件是caching_sha2_password,旧客户端不认识它。解决:优先升级客户端到支持8.0的版本;如果因为某些原因必须用旧客户端,可以创建用户时指定老的认证插件,但需要评估安全性。判断方法是执行SELECT user, host, plugin FROM mysql.user;查看用户认证方式。这是一个典型的“环境选择比代码更重要”的坑,实验课上一出现就是一大片人集体卡住。
5.3 加载外部数据文件时外键顺序颠倒导致导入失败
现象:用LOAD DATA或source导入课程资源包里的CSV文件时,先导入了选课表sc,报Cannot add or update a child row: a foreign key constraint fails。原因:sc表的外键引用student和course,而这两个主表的数据还没导入,子表先导必然违约。解决:严格按照主表→子表的顺序导入,先student再course最后sc;如果资源包给了一张总的导入清单但顺序是错的,手动调整顺序即可。临时关外键校验SET FOREIGN_KEY_CHECKS=0;也能绕过,但导完必须恢复成1,并且做一次全量校验,否则报告里写“数据完整”就是自己骗自己。
5.4 CHECK约束写了却不生效,非法数据照样进表
现象:建表时写了CHECK约束,插入明显超出范围的数据没有报错,比如给score插入9999分,MySQL只是默默接受。原因:你用的MySQL版本低于8.0.16,低版本会解析CHECK语法但不执行。解决:先执行SELECT VERSION();确认版本;如果确实在低版本上,用触发器或者应用层校验兜底。这个坑在课程实践里特别隐蔽,因为写报告的人往往会说“我加了CHECK所以数据安全”,但实际数据库行为与报告不符。更合理的做法是实验里直接说明“当前版本不支持强制CHECK,因此改用字段类型加应用层约束”,反而显得专业。
5.5 COUNT统计行数对不上,多半是算了NULL
现象:用COUNT(*)和COUNT(score)统计同一个表,两个结果不一致,差了正好是score字段为NULL的行数。原因:COUNT(字段)会跳过NULL值,只统计非NULL行数;COUNT(*)统计的是所有行数。解决:统计表总行数时一律用COUNT(*);只有明确想统计某个字段的非空数量时才用COUNT(字段)。这个坑在GROUP BY分组统计时最迷惑——某个分组在结果里消失,不是数据丢了,而是该组里目标字段全是NULL。做聚合实验时,先看SUM、AVG和COUNT的组合输出,一般能自查出来。
6. 收尾检查:一条命令确认实验库完整与索引真实生效
6.1 用 information_schema 给整个实验库做体检
实验做完、报告写完初稿,先别急着交,用一条SQL把整个库的状态拉出来看一眼:
SELECT table_name, table_rows, engine, table_collation FROM information_schema.tables WHERE table_schema = 'school';这张表会告诉你每个表的行数、存储引擎和字符集。table_rows是估算值,和实际行数可能略有偏差,但足够用来发现明显问题——比如某个表行数显示为0,而你明明导过数据,那就要检查是不是导进了别的库。字符集列能帮你确认每张表都是utf8mb4,不会出现某张表单独是latin1的“混血”情况。如果需要精确行数,再对每张表跑SELECT COUNT(*)。这是交实验报告前的固定体检流程。
6.2 让EXPLAIN结果和实际耗时互相印证
索引实验做到最后,很多人只看EXPLAIN的输出就下结论,其实还差一步:把索引的真实效果用实际耗时验证出来。在数据量不够大的情况下,索引带来的性能提升可能只有几毫秒,肉眼几乎看不出来。可以用MySQL的profiling功能生成精确的执行耗时:
SET profiling = 1; SELECT COUNT(*) FROM sc WHERE student_id = 42; SHOW PROFILES;SHOW PROFILES会列出刚才执行的SQL以及每一条的耗时,单位是秒,精度到小数点后好几位。先在没有索引的情况下跑一次记录耗时,再建索引跑一次,对比两次数值,比EXPLAIN输出更直观。我的习惯是EXPLAIN看执行计划、profiling看实际耗时,两者互相印证——执行计划说用了索引,耗时也确实降了,结论才算闭环。
另一个我坚持的收尾习惯是把每次实验的DDL和DML脚本按编号保存,比如01_schema.sql、02_data.sql、03_index.sql。报告里的SQL从这些文件复制,库里跑的也是这些文件,保证报告和真实库完全一致。以前做数据库实践时,有同学报告里写了一段很漂亮的索引优化SQL,但库里根本没建那个索引,答辩时被现场问住,场面很难看。现在每次实验结束,我都会把脚本归档一遍,把隔离级别、索引、约束这些关键配置的状态查一遍再收工。
这套依赖“可观察、可复现”的实践方法,我后来做别的数据库项目时也一直沿用。环境先行、工具验证、脚本留痕,三个习惯加在一起,能让你的数据库系统原理实践少走很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取