ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据库概论实验报告:从E-R图到事务隔离级别的完整实践与踩坑指南

数据库概论实验报告:从E-R图到事务隔离级别的完整实践与踩坑指南

简介:这是太原理工大学软件工程专业数据库系统原理课程的实验报告,围绕交互式SQL语句使用、数据完整性、用户鉴别与数据控制三个实验展开。报告以SQL Server 2016为平台,从创建、修改、删除基本表开始,逐步完成索引建立、视图创建、数据插入与更新,并附有完整的SQL命令和运行结果;内容既覆盖了表结构设计、主外键约束、唯一与聚簇索引等基础操作,也体现了权限控制与数据安全要点。压缩包共1个文件,为docx文档,大小约1.69MB,排版清晰,便于直接阅读和对照练习;目前已有311人学习下载。这份报告既适合正在学习数据库概论的学生复习SQL语法、完成实验作业,也可作为撰写数据库实验报告的参考模板,帮助读者快速掌握数据库对象管理与数据维护的完整流程。

1. 一份数据库实验报告,为什么值得当成项目来做

数据库概论实验报告,在很多同学眼里就是“老师布置的作业”,交上去打分、归档、完事。但如果你把它当作一个完整的项目来拆解——需求分析、概念设计、逻辑设计、物理实现、数据填充、查询验证、异常排查——你会发现这份报告几乎是数据库开发全流程的微缩沙盘。我见过不少工作两三年的开发者,回过头来被问“你数据库的隔离级别到底解决什么问题”,答得磕磕绊绊,倒是当年实验报告里一笔一画写的并发问题让他们捡回了记忆。这篇笔记就用某高校软件工程专业的“数据库概论实验报告”为样例,把从建库到写报告的整套流程、参数设置和踩坑点拆开讲。适合正在做课程实验的同学,也适合带实验的助教拿来当排查清单。

2. 实验环境与数据库设计:先把地基打对,后面才有得写

2.1 选型:为什么课程实验默认用 MySQL 8.x 而不是 5.7

课程实验最常见的数据库选型就是 MySQL,原因很简单:安装快、文档多、语法跟教材里的标准 SQL 偏差小。但版本选择值得说一句。MySQL 5.7 在很多实验室机器上还是默认版本,可如果你在实验报告里写“使用 MySQL 5.7”,那么窗口函数(ROW_NUMBER、RANK 这类)就没法演示,因为 5.7 不支持。而实验题目里只要出现“查询每个部门工资排名前三的员工”这类需求,8.x 的窗口函数就是最简洁的写法。我一般建议选型写 MySQL 8.0 以上版本,并安到本地或者某台实验服务器上,理由是它同时支持传统 DDL/DML 和新式分析函数,报告能覆盖的知识面更完整。

安装这一步,Linux 环境下常见的做法是直接用 apt 或 yum 拉官方源安装,注意把 default-authentication-plugin 设置成 caching_sha2_password 还是 mysql_native_password 要先想好。很多图形化客户端(比如老版本的 Navicat)对 caching_sha2_password 支持不好,连接时报错会让你误以为是密码错了。我一般建议实验环境统一用 mysql_native_password,虽然 8.x 默认不是它,但兼容性最好,避免把时间耗在连不上库这种和环境无关的问题上。如果是 Windows 上做实验,官方安装包选 Server only 即可,Workbench 可有可无,后面用命令行加一个 DBeaver 之类的轻量客户端就够用。

选型完成之后,实验报告里需要有一小节描述环境。这不是凑字数,而是让报告可复现。常见写法是列一张表,包括数据库名、版本、操作系统、客户端工具、字符集。字符集建议统一 utf8mb4,因为如果实验数据里插入 emoji 或者生僻字,utf8mb3 会直接报错,这也算报告里的一个加分细节。

2.2 E-R 图与关系模式:实验报告的“第一张脸”

数据库概论实验的第一步几乎都是做概念设计,也就是画 E-R 图。很多同学跳过画图直接建表,到了写报告的时候发现没东西可写。一个真实的教训是:某同学的实验题目是“设计一个图书管理系统数据库”,他上来就建了三张表(图书、读者、借阅),结果写范式分析时发现第三范式根本没法满足,因为借阅记录里的读者姓名和图书书名都是冗余的。这是典型的没做 E-R 图导致的后果——实体、属性、联系没理清,关系模式自然跟着乱。

正确的顺序是,先列实体:读者(读者号、姓名、单位)、图书(书号、书名、出版社、价格)、管理员(工号、姓名)。再列联系:借阅是读者和图书之间的多对多联系,而且有属性“借书日期”“还书日期”。“管理员”和“借阅”之间是管理员操作的接触。E-R 图完成后再转换成关系模式,转换规则教材里都有:多对多联系单独成一个关系,属性是两端的主码加联系本身的属性,比如借阅(读者号、书号、借书日期、还书日期、管理员工号)。这一步看起来琐碎,但实验报告最核心的评分点就在这里,评卷老师先看 E-R 图合不合理,再看关系模式转换对不对。

画图工具有 ProcessOn、draw.io 都行,也可以手画拍照贴进报告。我建议用 draw.io,导出的矢量图放进 Word 里清晰不糊。关系模式建议写成如下形式,并标注主码和外码:

读者(读者号,姓名,单位),主码:读者号
图书(书号,书名,出版社,价格),主码:书号
借阅(读者号,书号,借书日期,还书日期,管理员工号),主码:读者号+书号+借书日期,外码:读者号、书号

这段内容放在报告里,老师一眼就能看出你理解了“实体—联系—关系模式”的转化链路,比贴二十张截图管用得多。

2.3 建库建表 SQL:几条语句背后的字符集与约束设计

环境准备好、E-R 图定稿后,就到了动手建库建表的环节。建库语句本身很短,但有几个选项会在你后续执行实验时变成拦路虎。常见的建库 SQL 我一般写成下面这样:

CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE library_db;

逻辑说明:指定 utf8mb4 字符集是为了支持中文和特殊符号,_general_ci 是大小写不敏感的排序规则,适合课程实验里模糊匹配的需求。如果不指定 COLLATE,某些情况下表连接时比对字符串会带来排序规则冲突的报错,提前统一能省掉很多麻烦。

建表语句需要在字段后面直接把约束写上。图书表、读者表、借阅表三张核心表可以这么写:

CREATE TABLE reader ( reader_id CHAR(8) PRIMARY KEY, reader_name VARCHAR(20) NOT NULL, unit VARCHAR(50) ) ENGINE=InnoDB; CREATE TABLE book ( book_id CHAR(10) PRIMARY KEY, book_name VARCHAR(50) NOT NULL, publisher VARCHAR(30), price DECIMAL(6,2) CHECK (price >= 0) ) ENGINE=InnoDB; CREATE TABLE borrow ( reader_id CHAR(8), book_id CHAR(10), borrow_date DATE, return_date DATE, admin_no CHAR(6), PRIMARY KEY (reader_id, book_id, borrow_date), FOREIGN KEY (reader_id) REFERENCES reader(reader_id), FOREIGN KEY (book_id) REFERENCES book(book_id) ) ENGINE=InnoDB;

逻辑说明:CHAR 和 VARCHAR 的选择要讲得出道理,主码用定长 CHAR 是为了检索速度,姓名这类变长字段用 VARCHAR 节省空间。DECIMAL(6,2) 表示价格最多 9999.99,范围对图书定价够用。CHECK 约束在 MySQL 8.0.16 之后才真正生效,如果你用的是 8.0 以下版本,这个约束会被忽略但也不报错,这一点可以在报告的“实验环境”里注明,反而体现你对版本差异的了解。ENGINE=InnoDB 是必须写的,因为外码约束只有 InnoDB 支持,MyISAM 建外码语句执行后既不报错也不生效,属于比较隐蔽的一个坑。

3. 数据填充与 SQL 查询实验:把报告从“能跑”做到“能讲”

3.1 用 INSERT 和 UPDATE 构造数据时要注意外码顺序

实验数据怎么填,是很多人忽略但实际最花时间的环节。如果按“先读者、再图书、最后借阅”的顺序插入,就不会触发外码冲突;但如果先插入借阅记录,MySQL 会直接报 foreign key constraint fails。我看到有同学在这个报错上卡了半小时,最后才发现是插入顺序问题。常见做法是把插入脚本拆成三段,分别对应三张表,并且用显式事务包起来,保证中途出错可以整体回滚。插入语句示例如下:

START TRANSACTION; INSERT INTO reader VALUES ('R001', '张三', '软件工程系'), ('R002', '李四', '软件工程系'), ('R003', '王五', '计算机科学系'); INSERT INTO book VALUES ('B001', '数据库系统概论', '高等教育出版社', 45.00), ('B002', '计算机网络', '电子工业出版社', 39.50); INSERT INTO borrow VALUES ('R001', 'B001', '2024-03-01', NULL, 'A01'), ('R002', 'B002', '2024-03-02', '2024-03-16', 'A02'); COMMIT;

逻辑说明:NULL 放在 return_date 表示该书还没归还,这是用 NULL 表达“当前未还”的常见做法。事务的目的不是炫技,而是让你在不完全确定数据正确时能快速回滚重来。如果你在实验报告里写了“通过事务保证数据一致性”,评卷老师会默认你理解了事务的原子性,这是第一个能和别人拉开差距的细节。

INSERT 本身不是实验的难点,难点在于后续的 UPDATE 和 DELETE 会不会受外码限制。比如你想删掉 R001 这位读者,但 borrow 表里还有他的借阅记录,DELETE 会报错。常见的解决办法是先处理借阅记录再删读者,这也正好是实验报告里“数据的完整性控制”这一节的素材。

3.2 单表查询与多表连接:必写的几个查询类型

数据库概论的实验要求里,单表查询、连接查询、嵌套查询、集合查询基本是标配。很多报告把三条 SELECT 语句一贴就完事,缺少对每种查询类型的意图说明。我建议报告里按查询类型分小节,每类写一条有代表性的语句并解释执行过程。比如单表查询里最值得写的是带聚合函数和 GROUP BY 的查询:

SELECT unit, COUNT(*) AS reader_count FROM reader GROUP BY unit HAVING COUNT(*) >= 2;

逻辑说明:WHERE 在分组前过滤,HAVING 在分组后过滤,这是教科书上的原话,但只有你亲手写过一条 HAVING 语句,才会理解分组后过滤到底是什么场景。上面这条语句的语义是“找出读者数不少于 2 人的单位”,去掉 HAVING 条件结果会完全不同。

连接查询是重点,因为多表连接涉及连接类型的选择。最典型的一道实验题是“查询借阅了图书的学生姓名和书名”,写出下面的语句只是及格水平:

SELECT reader.reader_name, book.book_name FROM borrow JOIN reader ON borrow.reader_id = reader.reader_id JOIN book ON borrow.book_id = book.book_id WHERE borrow.return_date IS NULL;

逻辑说明:JOIN 的顺序是从 borrow 出发,先连读者得到谁借的,再连图书得到借的什么。WHERE borrow.return_date IS NULL 表示筛选未归还的记录。如果你想在报告里体现对连接的理解,还可以补一句:为什么没选 LEFT JOIN?因为内连接只返回借阅表里匹配成功的记录,左连接会把没有借阅记录的读者也带出来,两种结果集的口径完全不同,写报告时要把这种取舍写清楚。

嵌套查询和 EXISTS 也建议各写一条。比如“查询没有借阅过任何图书的读者”,IN 和 NOT EXISTS 两种写法均可以,但 NOT EXISTS 在语义上更贴近“不存在”的判断,性能在数据量大时也通常优于 NOT IN,因为 NOT IN 遇到 NULL 值会返回空结果。这个坑非常经典,值得写进实验报告成为你的“思考题”部分。

4. 进阶实验:视图、事务、存储过程与触发器怎么选型

4.1 视图实验:为什么我们建视图而不直接查表

实验报告里通常要求建一两个视图。很多同学不理解视图的意义,觉得就是把一条 SELECT 语句存起来,多此一举。实际差别在于:视图提供了逻辑独立性、安全性控制和简化查询三层价值。比如给辅导员查“本系学生的借阅情况”,如果直接给基表的查询权限,他就能看到所有学生的所有数据;但建一个视图把它限定在“软件工程系”范围内,再授 SELECT 权限给他,他就只能看该看的数据。

CREATE VIEW v_se_student_borrow AS SELECT reader.reader_name, book.book_name, borrow.borrow_date FROM borrow JOIN reader ON borrow.reader_id = reader.reader_id JOIN book ON borrow.book_id = book.book_id WHERE reader.unit = '软件工程系';

逻辑说明:视图本身不存储数据,执行查询时临时做连接运算,所以视图并没有“空间换时间”的效果。这一点经常被误解。数据量大的时候,基于视图的查询不一定比直接查基表快,它主要是逻辑层的东西,这一点写进报告还能体现你的理解深度。视图能不能更新取决于它是否满足“可更新视图”的条件,比如不能有 GROUP BY、DISTINCT 这类破坏行对应关系的元素,很多同学在视图上跑 UPDATE 失败后以为是自己写错了,其实这是 MySQL 对更新规则的限制。

4.2 事务与隔离级别:实验报告里最容易被追问的一块

事务实验是数据库概论的重点,也是面试常考。建议实验里用两个终端窗口连同一个数据库,模拟两个并发事务。最经典的实验是“买书扣库存”场景:将图书数量减 1 和插入借阅记录放在一个事务中。两个终端同时执行,观察不同隔离级别下的行为差异。我第一次带实验时就让 A 同学开了两个会话分别执行下面的逻辑:

-- 会话 1 START TRANSACTION; UPDATE book SET price = price + 10 WHERE book_id = 'B001'; -- 此处不提交,等会话 2 SELECT * FROM book WHERE book_id = 'B001';
-- 会话 2 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT price FROM book WHERE book_id = 'B001';

现象是:会话 2 在 READ UNCOMMITTED 下读到了会话 1 尚未提交的修改,也就是脏读。把隔离级别改成 READ COMMITTED 后,会话 2 读到的还是旧值,脏读消失。这个对照实验写在报告里非常有说服力,因为它直观地展示了隔离级别存在的意义。但要注意:MySQL 默认隔离级别是 REPEATABLE READ,不是 READ COMMITTED;实验时把会话 2 的隔离级别显式设置成不同值并记录结果,才是完整的实验。下面是一个查看隔离级别的语句:

SELECT @@transaction_isolation;

逻辑说明:这条语句返回当前会话的事务隔离级别。做实验前先执行它,确认环境状态,再做并发测试,避免因默认值不同导致现象解释出错。实验报告里还应该写上结论:隔离级别越高,一致性越好,并发能力越弱;脏读、不可重复读、幻读三种异常分别对应 READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ 的不设防点,MySQL 的 REPEATABLE READ 通过 MVCC 解决了幻读的大部分场景,但间隙锁仍在特定条件下发挥作用。

4.3 存储过程和触发器:要不要写进报告

如果实验大纲要求了存储过程或触发器,那么选哪个场景写才最有说服力?我建议选一个“需要被重复调用的业务规则”,比如批量登记借阅记录。存储过程的好处是逻辑封装在数据库端,客户端不需要重复写多行 SQL。触发器则适合做“自动校验”的场景,比如借书时必须保证该读者当前未借满 5 本,超过则拒绝。下面这个触发器可以作为样例:

DELIMITER // CREATE TRIGGER trg_borrow_limit BEFORE INSERT ON borrow FOR EACH ROW BEGIN DECLARE cnt INT; SELECT COUNT(*) INTO cnt FROM borrow WHERE reader_id = NEW.reader_id AND return_date IS NULL; IF cnt >= 5 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'borrow limit exceeded'; END IF; END// DELIMITER ;

逻辑说明:NEW.reader_id 表示待插入的新行中的读者号,BEFORE INSERT 表示在插入前做校验;SIGNAL SQLSTATE '45000' 是 MySQL 主动抛异常的标准写法,如果不用它,触发器只能静默失败,调用方完全不知道发生了什么。写报告时值得加一句:触发器有额外开销,而且在数据量大的批处理场景里会成为性能瓶颈,生产环境里很多团队宁可在应用层做同样的事,但在课程实验里写触发器可以充分展示触发器的机制理解。

5. 实验路上的常见坑与排查清单:现象、原因、解法

5.1 中文乱码和数据插不进去

现象:插入中文后 SELECT 出来是问号 ?,或者 INSERT 语句直接报错 “Incorrect string value”。

原因:客户端连接字符集不是 utf8mb4,或者数据库、表的字符集不统一。最常见的是数据库建库时设了 utf8mb4,但客户端工具连接用的还是 latin1。

解决:建库时统一写 CHARACTER SET utf8mb4,连接后在执行插入前先执行 SET NAMES utf8mb4;用命令行连接时加 --default-character-set=utf8mb4 参数。字符集问题排查可以一次到位,不建议在报告里只贴一张“乱码截图”了事,要把排查链路写清楚。

5.2 外键插入失败:foreign key constraint fails

现象:向从表插入数据时 MySQL 报错,但检查数据感觉没问题。

原因:最常见的是从表引用的主表主码值不存在,或者两张表的字段类型不一致。比如 reader_id 在主表是 CHAR(8),在从表却定义成了 VARCHAR(8),虽然看起来能关联,但 MySQL 对外键字段类型要求严格,字符集的差异也会导致这个错误。

解决:先查主表中对应字段是否存在该值,再核对字段类型和字符集完全一致。排查时用下面这条语句查看表结构定义,重点比较两个字段的类型定义:

SHOW CREATE TABLE borrow;

逻辑说明:这条语句会返回完整的建表语句,外键定义、字符集、约束一眼就能看到。用它排查比反复猜效率高得多。

5.3 NOT IN 与 NULL 值导致查询结果为空

现象:查询“没借过书的读者”返回结果为空,明明是空表却有数据。

原因:borrow 表里只要有任意一个读者号为 NULL,NOT IN 子查询的结果集里包含 NULL,整个 NOT IN 条件就不会返回任何行。这是 SQL 三值逻辑的体现,很多同学第一次遇到都很难理解。

解决:子查询过滤掉 NULL,比如 NOT IN (SELECT reader_id FROM borrow WHERE reader_id IS NOT NULL);或者换成 NOT EXISTS 写法,不存在 NULL 问题。这一现象很适合写进报告的“实验心得”部分,能明显提升报告的非机械感。

5.4 DELETE 基表数据被外码约束拒绝

现象:删除某个读者时 MySQL 报错 Cannot delete or update a parent row: a foreign key constraint fails。

原因:从表存在引用该读者的记录,外码约束阻止了主表删除。

解决:按依赖顺序处理,先删除或更新从表的相关记录,再删主表;或者在设计外键时指定 ON DELETE CASCADE。课程实验里不推荐用 CASCADE,因为可能造成意外的大批量删除;报告里写清楚“通过先删子表数据维护了引用完整性”,反而是加分项。

5.5 排序规则冲突:Illegal mix of collations

现象:两张表连接时报错,涉及字段的排序规则不一致,尤其出现在不同字符集下。

原因:建表时有的字段用了 utf8mb4_general_ci,有的用了 utf8mb4_unicode_ci,内连接时 MySQL 无法自动兼容。

解决:统一所有表字段的排序规则。查看字段排序规则用 SHOW FULL COLUMNS FROM 表名,修改字段排序规则用 ALTER TABLE ... MODIFY。

6. 把实验报告写“厚”:从提交物到可翻查的积累

很多同学实验做完了,报告却只写几段 SQL 和几张截图,有点浪费。因为数据库实验报告是少有的能同时覆盖业务设计、SQL 编写和故障排查三块内容的课程作业。我觉得至少应该补三样东西:一是数据字典表,把所有表名、字段名、类型、约束、说明列成表,这相当于一个小型接口文档;二是关键查询的“验证方法”,即每写完一条复杂 SQL,要写清楚你如何确认结果是正确的,比如对聚合结果做手工核算、用 LIMIT 抽查数据等;三是结合实验数据写一个分析结论,比如“从借阅表中统计热门出版社”,这种带业务视角的小结论,让报告不只是操作记录。这三样补进去之后,报告才是完整的。

另外建议保留每次实验的原始命令和报错截图,存成一个“排错记录”文件夹。哪怕实验已经交掉,过半年面试前翻出来,你会发现这些真实报错比任何面经都记忆深刻。我自己就有这个习惯:某次实验里 NOT IN 和 NULL 的坑,让我在一次面试手写 SQL 时躲过了同样的陷阱。希望这个建议也能帮到你,实验不只是为了学分,把每次踩坑都转化成自己的技术沉淀,后面你会感谢当初这份认真的自己。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进