简介:面向西安交通大学计算机专业数据库系统课程的Lab作业资源包,适合正在学习数据库原理、SQL编程及数据库应用开发的学生参考。内容涵盖E-R概念模型设计、关系模式规范化、SQL的DDL/DML/DQL/DCL操作,以及基于JDBC/ODBC思路的Python联调实验。资源共37个文件,包括11个Python脚本、12张PNG与9张JPG截图、2个Markdown说明文档等,压缩包仅5.86MB,目录结构清晰,便于按模块对照学习。脚本涉及建库建表、数据初始化、复杂查询、批量插入、更新删除、视图与存储过程等环节,截图记录了关键运行结果和调试输出。已有55人学习下载,适合课程实验参考、期末复习或补交作业时快速梳理核心实现思路。
1. 数据库系统的 Lab 作业:这份西安交大资源包里到底有什么
做数据库系统课设的人大概都经历过这种状态:教材翻了三遍,SQL 语法背得滚瓜烂熟,一打开实验文档发现无从下手。西安交大计算机系的数据库系统 Lab 作业,正好就是这么一份能直接对照着做的完整资源包。它不是那种只丢给你一堆建表语句的习题集,而是从 SQL 查询、ER 图设计到事务与并发控制的整条实验主线,每一份文档对应一个具体的实验验收点。
这份资源适合两类人:一类是正在上数据库系统概论、需要应付课程实验但在线环境里卡住的人;另一类是已经工作、想快速复盘数据库核心实验细节的人。它帮不了你应付面试里的系统设计,但能帮你把学校里要求的那几个实验扎实做完——前提是你愿意自己动手敲一遍,而不是把里面的代码原样交上去。
2. 拆包之后的真实结构:四个 Lab 对应的知识点与验收主线
拿到 zip 之后,第一步不是急着解压看代码,而是先把目录结构理清楚。西安交大这套 Lab 作业的命名方式和实验安排,直接对应《数据库系统概念》第七版的几个核心章节——SQL 基础与聚合查询、ER 模型转关系模式、事务隔离级别、以及存储过程与触发器。文件结构通常是这样组织的:
lab/ ├── lab1_sql_basics/ │ ├── schema.sql │ ├── queries.sql │ └── lab1_report.md ├── lab2_er_design/ │ ├── er_diagram.drawio │ ├── er_to_relational.sql │ └── lab2_report.md ├── lab3_transaction/ │ ├── tx_demo.sql │ └── lab3_report.md └── lab4_trigger_procedure/ ├── trigger_demo.sql └── lab4_report.md大多数情况下,Lab 1 的核心是让你在一个给定的 schema 上完成若干条 SQL 查询。关键不在查询本身,而在考察分组聚合、子查询和连接三者的组合使用。比如统计每个系的学生人数、查出选修课超过三门的学生名单这类问题。Lab 2 则要求根据一段文字描述画出 ER 图,再把它转成关系模式,这部分牵扯到多值属性和弱实体的处理。Lab 3 和 Lab 4 是后期重点,涉及事务的隔离与触发器的编写,需要你在 MySQL 或 PostgreSQL 里实际验证,理解隔离级别的行为差异。
我拆这个包的时候发现,里面有一份 README 明确写了每个 Lab 的验收标准:哪些查询需要返回精确的行数、哪些字段不能为 NULL、触发器要处理哪几种边界情况。这些细节是这个资源最有价值的部分——很多人做完实验根本不知道评分点在哪里,对着这份清单一条一条过,比盲目刷题高效得多。
3. 把 SQL 练习当成黑盒测试:从错误信息反推数据陷阱
Lab 1 这类 SQL 练习,很多人一上来就写查询,写完一跑发现结果不对,然后开始一行一行改。我一般不会这么做。正确的打开方式是先把给定的 schema 读透,把每一张表的约束条件、外键关系、以及可能存在的 NULL 值陷阱全部标出来,然后再写查询。比如“查出所有没有选课的学生”这道题,如果直接用 NOT IN 子查询,遇到选课表里存在 NULL 学号的情况,结果会是空集——这不是 SQL 语法问题,是逻辑问题。
-- 错误写法:student_id 在选课表中可能为 NULL SELECT * FROM student WHERE student_id NOT IN (SELECT student_id FROM course_selection); -- 正确写法:使用 NOT EXISTS 规避 NULL 陷阱 SELECT * FROM student s WHERE NOT EXISTS ( SELECT 1 FROM course_selection cs WHERE cs.student_id = s.student_id );这里的关键在于 NOT IN 和 NOT EXISTS 的执行语义完全不同。NOT IN 子查询返回的结果集里只要出现一个 NULL,整个查询的结果就会变成空集,因为 SQL 的三值逻辑里,NULL 和任何值比较都是 UNKNOWN,而 WHERE 子句只保留 TRUE 的结果。NOT EXISTS 是逐行关联判断,即使子查询里有 NULL,只要关联条件不匹配就能正常返回。实际跑数据的时候,选课表里经常会出现迟迟不录入分数的记录,学号被临时留空,这种看似不起眼的脏数据足够让一版看起来正确的查询直接翻车。
聚合查询是另一个高频失分点。统计每个班级的平均分、最高分这类问题,初学者容易忘记 GROUP BY 和聚合函数的配合规则。MySQL 默认开启了 ONLY_FULL_GROUP_BY,也就是说 SELECT 后面出现的非聚合列,必须出现在 GROUP BY 子句里。如果拿到一个报错信息是“which isn't in GROUP BY clause”,别急着去改 SQL 模式,先检查自己的查询是否违反了这条规则。
-- 反例:班级名未出现在 GROUP BY 中 SELECT class_id, class_name, AVG(score) FROM student_score GROUP BY class_id; -- 正解:要么把 class_name 加进 GROUP BY,要么去掉这个字段 SELECT class_id, AVG(score) FROM student_score GROUP BY class_id;FROM 子句里的多表连接也是拉分项。INNER JOIN 和 LEFT JOIN 的选择标准,取决于你希望保留哪一侧的数据。做“每个学生的选课门数”统计时,如果一个学生没有选任何课,INNER JOIN 会直接把他丢掉,LEFT JOIN 则会保留学生信息并显示选课门数为 0。很多实验文档刻意设置了这种边界数据,就是考察你对连接方向的理解是否到位。
这个 Lab 的调试环境和在线评测系统不太一样,本地跑完和提交上去的结果可能不一致。拿去重、排序、LIMIT 这类操作时,如果评测系统用的是 PostgreSQL,而本地环境是 MySQL,两者的行为在某些边界上有区别。PostgreSQL 对 NULL 的排序默认放在最后,MySQL 则放在最前。遇到排序结果不一致,优先检查 NULL 的排列位置和你自己写的 ORDER BY 条件。
4. ER 图设计题的三类致命坑:从“看起来对”到“一查就崩”
Lab 2 的 ER 图设计,是整套作业里最玄学的一个环节。很多人画 ER 图时觉得实体、属性和关系都标清楚了,但一转换成关系模式就暴露出问题。最常见的坑是弱实体的依赖关系没有体现出来。比如“订单明细”依赖“订单”而存在,脱离了订单这个强实体,明细就没有独立的主键意义。转换成关系模式时,弱实体的主键必须包含强实体的主键,否则查询时无法关联出完整的上下文。
三元关系是另一个容易出问题的地方。一个“供应商-零件-项目”的三元关系,表示的是供应商为某个项目供应某种零件,这时不能拆成三个二元关系来处理。拆开会丢失约束:某个供应商确实供应零件 A 和零件 B,但可能只向项目 X 供应 A,同时向项目 Y 供应 B。如果你拆成了三个独立的二元关系,数据库就无法阻止这个供应商向项目 X 也供应了零件 B。这种数据约束的丢失,在设计阶段看不出来,等到业务数据录入后才会暴露。
多值属性的处理也值得单独说。一个人有多个电话号码,如果把电话号码直接作为用户的属性列,查起来会非常痛苦。正确的做法是拆出一张独立的电话表,用外键关联用户 ID。属性与实体的边界划分,直接决定了后续 SQL 的写法是否自然。如果你发现自己的查询里频繁用 GROUP_CONCAT 或者字符串拼接来凑属性值,大概率是设计阶段就把多值属性压进了单列。
ER 图转关系模式的规则其实很机械:一对一关系可以合并到任意一端;一对多关系把“一”端的主键放到“多”端作为外键;多对多关系必须拆成一张独立的关系表,关系表的主键由两端主键联合构成。把这几条规则搞清楚,再回头看实验文档里的要求,会发现很多题根本不用凭感觉画,直接按规则推导就能得出标准答案。
-- 多对多关系的标准转换:选课关系表 CREATE TABLE course_selection ( student_id INT NOT NULL, course_id INT NOT NULL, semester VARCHAR(20), 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) );联合主键的设计在这个场景下是对的,但也要考虑实际业务的扩展性。如果一个学生同一个学期选了同一门课两次(补考、重修),联合主键就会冲突。我在实际项目中一般会额外加一个自增 ID 作为代理主键,保留 (student_id, course_id) 的唯一索引来约束业务规则。学校实验不会考察到这个深度,但如果你以后要去做真实的业务系统,这一步提前想清楚能省掉后面很多数据清洗的麻烦。
5. 事务与并发:隔离级别验证中的四个典型的翻车现场
Lab 3 事务实验的难度比前面两个 Lab 明显上了一个台阶。这里的核心不是让你背四种隔离级别的定义,而是要在真实数据库里跑出对应的并发现象,并且解释清楚为什么会这样。MySQL 默认的隔离级别是 REPEATABLE READ,PostgreSQL 默认是 READ COMMITTED,很多人在本机验证时没注意这个差异,导致结果跟实验文档对不上。
先看脏读。脏读的本质是一个事务读到了另一个事务未提交的数据。在 READ UNCOMMITTED 级别下会发生,但 MySQL 的 InnoDB 存储引擎实际上在这个级别也不会让你读到物理上未提交的修改,因为锁机制本身就阻止了部分访问。所以用 MySQL 验证脏读,你很可能复现不出来。这也是为什么实验文档里通常建议你用两个独立的终端窗口,一边开事务执行 UPDATE 但不 COMMIT,另一边在同一时刻执行 SELECT,看能不能读到修改后的值。
不可重复读是另一种现象,描述的是同一事务内两次相同的 SELECT 返回了不同结果,原因是另一个事务在此期间提交了修改。这个在 READ COMMITTED 级别下很容易复现:
-- 终端 A:开启事务,先读一次 BEGIN; SELECT balance FROM account WHERE id = 1; -- 假设读到 100 -- 终端 B:提交一个修改 UPDATE account SET balance = 200 WHERE id = 1; COMMIT; -- 终端 A:再次查询,读到 200,不可重复读发生 SELECT balance FROM account WHERE id = 1;我在给读者复现这个实验时,经常遇到一个情况:明明开了两个终端,但读到的值始终不变。排查了半天,发现有人把事务隔离级别设置语句写错了,把 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED 写成了 SET TRANSACTION ISOLATION LEVEL READ COMMITTED。前者只对当前会话生效,后者是对下一个事务生效,两者的作用范围完全不一样。
幻读是实验报告里最容易混淆的概念。幻读和不可重复读的区别在于,不可重复读针对的是同一行数据的值发生变化,幻读针对的是结果集的行数发生变化。比如事务 T1 里执行 SELECT * FROM orders WHERE amount > 100,返回了 3 行;事务 T2 插入了一条 amount = 200 的新记录并提交;T1 再次执行同样的查询,返回了 4 行。这个现象在 REPEATABLE READ 级别下,InnoDB 通过间隙锁可以部分规避,但如果用的是 PostgreSQL,REPEATABLE READ 下你根本复现不了幻读,因为它的快照隔离机制已经把查询结果固定住了。
死锁是实验里最常出现的非预期结果。两个事务各自持有对方需要的行锁,互相等待,最终数据库会自动检测并回滚其中一个事务。我在做这个实验时,故意构造了 AB-BA 的加锁顺序来触发死锁,然后看 MySQL 的错误日志:
-- 事务 A:先锁 id=1 的行,再锁 id=2 的行 BEGIN; SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 此时事务 B 已经锁住了 id=2 SELECT * FROM account WHERE id = 2 FOR UPDATE; -- 死锁发生,其中一个事务被回滚MySQL 的错误信息会显示“Deadlock found when trying to get lock”,并且会自动回滚被牺牲的事务。这里需要注意,被回滚的不一定是后发起请求的那个事务,InnoDB 会根据它自己计算出的代价来选择回滚对象。所以实验结果看起来像是随机的,但实际上有内在规则。做实验记录时,把两个终端各自的执行时序写清楚,比记录谁被回滚更有说服力。
6. 把 Lab 当工程做:验证脚本、提交检查与一条诚信底线
做完一遍实验只是第一步,真正拉开差距的是验证环节。我每次提交 Lab 前都会跑一遍自己的验证脚本,把每个实验的验收点自动过一遍。这个习惯是从一次惨痛经历开始的——那次我把一个 JOIN 的方向写反了,自查了三遍都没看出来,等助教跑评测脚本才发现结果全空。
#!/bin/bash # 验证脚本:检查 lab1 查询是否返回预期行数 echo "Running query check..." EXPECTED_ROWS=12 ACTUAL_ROWS=$(mysql -u student -p lab_db -N --batch -e "SELECT COUNT(*) FROM (某个查询)" 2>/dev/null | tr -d '\n') if [ "$ACTUAL_ROWS" -eq "$EXPECTED_ROWS" ]; then echo "[PASS] query returned expected rows" else echo "[FAIL] expected ${EXPECTED_ROWS}, got ${ACTUAL_ROWS}" fi验证脚本的价值在于,它把人工比对结果变成机器比对,避免了肉眼判断带来的侥幸心理。脚本里我刻意加了一个tr -d '\n'的管道操作,因为 mysql 客户端在 batch 模式下输出结果时会带一个换行符,直接赋值给变量会把换行符也带进去,比较的时候永远不相等。这个坑我踩了一整晚,最后用 od -c 查看输出才定位到。
提交之前,还要检查一遍 SQL 脚本本身的可重复执行性。很多实验要求你提供的 schema.sql 能被重新导入,如果你的脚本里写了CREATE TABLE IF NOT EXISTS,重复执行不会报错,但如果用的是CREATE TABLE,第二次导入就直接中断。这属于细节问题,但恰恰是评分时最容易扣分的地方。
-- 可重复执行的建表脚本:先 DROP 再 CREATE DROP TABLE IF EXISTS course_selection; CREATE TABLE course_selection ( student_id INT NOT NULL, course_id INT NOT NULL, PRIMARY KEY (student_id, course_id) );最后必须说一句:这份资源包是好东西,里面包含了完整的实验文档、可参考的 SQL 代码和设计思路,但它替代不了你的思考过程。数据库系统这门课的核心能力是你在处理数据时形成的逻辑判断力——一个 JOIN 该用哪种方向、一个约束该放在哪一层、一个并发场景该容忍哪种级别的不一致。这些能力只能靠自己的手去碰、自己的错误去喂。从那以后,我每次做完实验都会强制自己从头跑一遍验证脚本,再做一次代码走查,确保提交出去的每一份作业都经得起追问。希望帮到你。
本文还有配套的精品资源,点击获取