☰
MySQL成绩管理系统实验报告:从E-R建模到事务回滚的完整实践
2026/11/4 2:57:39 网站建设 项目流程

简介:这份PDF实验报告面向计算机相关专业学生与数据库课程学习者,围绕MySQL学生成绩管理系统的完整设计流程展开,可用于课程实验参考、项目设计借鉴或数据库课程复习。报告从项目背景与编写目的切入,依次覆盖可行性分析、功能与性能需求、数据库需求分析及数据字典设计,具体给出学生、教师、课程、班级、系等实体的字段类型、主外键与关联关系,并配有功能结构图与顶层数据流图,便于读者理解从需求到表结构的推导过程。资源包共1个PDF文件,约486KB,篇幅紧凑、结构完整,适合打印或电子阅读。目前已有10438人学习下载,说明其在同类实验报告中有较高参考价值,读者可据此梳理成绩管理系统的设计思路与文档撰写框架。

1. 一份能直接跑通的 MySQL 成绩管理实验报告,到底值不值得拆

期末周最让人头大的不是考试,而是考完之后那一周的成绩统计。一个班几十号人、七八门课,手工录 Excel 再算总分平均分,改一个数字整张表跟着重算,错一个学号能对半天。这份《MySQL 学生成绩管理系统设计实验报告》就是冲着这个场景来的:它把从需求分析、E-R 建模、建表建索引、视图存储过程、触发器、权限控制到事务回滚的完整链路走了一遍,技术栈是 MySQL 5.6 + Java + Eclipse + Navicat,对象是任课教师和学生两类角色。适合谁?正在做数据库课程设计、需要一份能照着复现的建库脚本和 SQL 逻辑的在校生,以及想快速回顾 MySQL 视图、存储过程、触发器、事务这几块实操细节的初级后端。它不是成品系统,是一份带 SQL 骨架的设计文档,价值在于那些能直接抄进 Navicat 里跑的语句和背后的设计取舍。

2. 从 E-R 到八张表:成绩库的建模逻辑与建表脚本

2.1 为什么是八张表而不是一张大宽表

很多人第一反应是建一张score表,学号、姓名、班级、课程、成绩全塞进去。这么干在录入阶段确实爽,但一到统计就翻车:同一个学生选五门课,姓名和班级要重复存五遍,改一次班级得 update 五行,漏一行就是数据不一致。这份报告拆成 Student、Teacher、Course、Class、Depart、Major、CV(选课)、ST(学生-教师-课程)八张表,本质是在做范式分解。

具体看依赖关系:学号决定姓名、性别、专业号、班号;班号又决定专业号,这里其实藏着一个传递依赖,报告里把 Student 标成第三范式、其余标 BCNF,严格说 Student 里班号->专业号这个链条让它离 BCNF 还差一点,但作为课程设计够用了。CV 表用(Sno, Cno)做联合主键,成绩依赖这个组合键,符合 BCNF。ST 表用(Sno, Tno)组合,课程号作为附加属性,这个设计稍微有点冗余——课程其实已经能通过 CV 关联到学生,ST 更多是为了记录"哪个老师教哪个学生的哪门课",实际项目里我会把它并进一个 teaching 关系表,但报告保留它也有教学意义:让你体会一下多对多关系怎么用中间表拆。

八张表的关系链是:系 → 专业 → 班级 → 学生,学生和课程通过 CV 多对多关联,教师独立成表。这个层级结构决定了后面所有查询的 join 路径,建模阶段想清楚,写 SQL 时就不用临时拼表。

2.2 建表语句逐段拆解与字段类型修正

报告给的建表脚本能跑,但有几处类型选择值得说道。先看原始写法:

CREATE TABLE Student1 ( Sname CHAR(20), Sno INT(5), Sex CHAR(2), Mno INT(5), Classno INT(5), PRIMARY KEY (Sno), Index Student1(Sno) );

这段能执行,但有两个坑。第一,INT(5)里的 5 不是长度限制,是显示宽度,MySQL 8.0 之后已经废弃这个写法,5.6 里写不写都一样,学号用INT就够,真要固定位数用CHAR(10)更稳,因为学号可能带前导零,INT 会把00123存成123。第二,主键上再建一个同名普通索引是多余的,InnoDB 的主键本身就是聚簇索引,重复建索引只浪费空间。我一般会改成:

CREATE TABLE student ( sno CHAR(10) NOT NULL COMMENT '学号,保留前导零', sname VARCHAR(20) NOT NULL COMMENT '姓名', sex CHAR(2) DEFAULT '男' COMMENT '性别', mno INT COMMENT '专业号', classno INT COMMENT '班号', PRIMARY KEY (sno), KEY idx_mno (mno), KEY idx_classno (classno) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

改动点说明:sno换CHAR(10)保住前导零;VARCHAR替代CHAR省空间,姓名长度不固定;外键字段mno、classno单独建普通索引,因为后面按专业、按班级统计成绩时这两个字段是高频过滤条件,不建索引就是全表扫。ENGINE=InnoDB必须显式写,5.6 默认虽是 InnoDB,但显式声明能避免换环境时被改成 MyISAM——MyISAM 不支持事务,后面的事务设计直接失效。字符集用utf8mb4而不是utf8,中文姓名和特殊符号都不会出乱码。

其余七张表同理,重点检查三处:CV 表的result字段报告用CHAR(5),成绩本质是数值,用DECIMAL(5,2)才能直接SUM、AVG而不用隐式转换,CHAR存"85.5"做聚合时 MySQL 会转数字,但一旦有空格或空串就变 0,统计结果悄悄错掉;Course 的credit用CHAR(2)也是同样问题,学分应该是TINYINT或DECIMAL(3,1);ST 表缺主键,报告只建了索引,实际应该PRIMARY KEY (sno, tno, cno)防重复插入。

建完表先跑一遍SHOW CREATE TABLE student;确认引擎和字符集,再插两条测试数据验证前导零没丢,这一步花两分钟,能省掉后面调试半天的血泪。

3. 视图、存储过程与触发器:把统计逻辑沉到数据库层

3.1 三个视图解决 80% 的查询需求

报告设计了三个视图:成绩查询视图 Rselect、学分统计视图 Total、总分平均分视图 sum。视图的价值在于把多表 join 固化下来,应用层直接SELECT * FROM 视图名就行,不用每次在 Java 里拼 SQL。看第一个:

CREATE VIEW v_score_detail AS SELECT s.classno, cv.sno, s.sname, cv.cno, cv.result FROM student s JOIN cv ON s.sno = cv.sno;

这个视图把学生信息和成绩拼在一起,教师查班级成绩、学生查自己成绩都走它。注意报告原文写的是From Student,CV Where Student.sno=CV.sno的隐式 join,能跑但可读性差,改成显式JOIN后加字段别名,后期维护一眼能看懂关联条件。

第二个视图统计每个学生修的课程和学分:

CREATE VIEW v_credit_total AS SELECT cv.sno, cv.cno, c.cname, c.credit FROM course c JOIN cv ON c.cno = cv.cno;

第三个是重点,算总分和平均分:

CREATE VIEW v_score_summary AS SELECT cv.sno, s.sname, SUM(cv.result) AS total_score, AVG(cv.result) AS avg_score FROM student s JOIN cv ON s.sno = cv.sno GROUP BY cv.sno, s.sname;

这里有个细节:GROUP BY必须带上sname,否则在ONLY_FULL_GROUP_BY模式下会报错,5.6 默认没开这个模式所以报告能跑,但换到 5.7 以上直接翻车。我习惯一开始就写全,避免迁移时踩坑。另外AVG遇到NULL成绩会跳过该行,如果某学生某门课还没录成绩,平均分是按已录课程算的,这个语义要在应用层说清楚,不然学生看到平均分虚高会来问。

视图建好后,教师端"查某班某课成绩排名"就是一句SELECT * FROM v_score_detail WHERE classno=? AND cno=? ORDER BY result DESC,不用再关心底层几张表怎么连。

3.2 存储过程:修改成绩和查总学分的封装

报告里存储过程部分原文有截断,Xiugai和Xuefen两个过程只写了一半。按它的意图补全,修改成绩的过程应该是:

DELIMITER // CREATE PROCEDURE sp_update_score( IN p_sno CHAR(10), IN p_cno INT, IN p_result DECIMAL(5,2) ) BEGIN DECLARE v_exists INT DEFAULT 0; SELECT COUNT(*) INTO v_exists FROM cv WHERE sno = p_sno AND cno = p_cno; IF v_exists = 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '该学生未选此课程,无法修改'; ELSE UPDATE cv SET result = p_result WHERE sno = p_sno AND cno = p_cno; END IF; END // DELIMITER ;

逻辑说明:先查选课记录是否存在,不存在就抛自定义错误,避免 update 影响 0 行却静默成功——这是成绩系统里最隐蔽的坑,老师以为改成功了,其实学号输错一位,页面没报错,成绩根本没动。SIGNAL SQLSTATE '45000'是 MySQL 抛业务异常的通用写法,Java 端 catchSQLException就能拿到 message 展示给用户。参数p_result用DECIMAL(5,2)和表字段对齐,传字符串"85"时 MySQL 会自动转,但传"八十五"就报错,这个边界在应用层做校验更友好。

查总学分的过程:

DELIMITER // CREATE PROCEDURE sp_total_credit(IN p_sno CHAR(10)) BEGIN SELECT s.sno, s.sname, IFNULL(SUM(c.credit), 0) AS total_credit FROM student s LEFT JOIN cv ON s.sno = cv.sno LEFT JOIN course c ON cv.cno = c.cno WHERE s.sno = p_sno GROUP BY s.sno, s.sname; END // DELIMITER ;

用LEFT JOIN而不是JOIN是关键:新生还没选课时,JOIN查出来是空结果集,前端拿到 null 会崩,LEFT JOIN配合IFNULL返回 0 学分,语义正确。这个细节报告没写,但实际项目里新生入学第一周就会遇到。

3.3 触发器:删课程时级联清理的边界

报告设计了一个删除课程后自动清理 CV 和 ST 的触发器:

DELIMITER // CREATE TRIGGER trg_course_delete AFTER DELETE ON course FOR EACH ROW BEGIN DELETE FROM cv WHERE cno = OLD.cno; DELETE FROM st WHERE cno = OLD.cno; END // DELIMITER ;

AFTER DELETE配OLD.cno拿被删课程的编号,逻辑没问题。但这里有个设计层面的追问:删课程连带删成绩,这个操作该不该让数据库自动做?如果误删一门课,几十个学生的成绩瞬间没了,触发器不会问你确不确定。我的做法是触发器只做软删除标记,或者干脆不用触发器,在应用层用事务包住"删课程 + 删关联成绩"两步,加一个确认弹窗。报告用触发器是为了演示语法,生产环境里这种级联删除要非常谨慎,血泪经验是:任何AFTER DELETE触发器上线前,先在测试库用SELECT把OLD值打出来确认范围,别直接在生产库试。

另外触发器里不能对本表做增删改,否则报Can't update table in stored function/trigger,这是 MySQL 的硬限制,设计级联逻辑时要绕开。

4. 权限、事务与回滚:成绩系统的安全底线怎么守

4.1 三类用户三种权限的最小授权

报告建了cu1、cu2、cu3三个用户,分别对应管理员、学生、教师,授权思路是对的,但WITH GRANT OPTION给多了。看原文:

GRANT ALL ON Stu.Student TO 'cu1'@'localhost' WITH GRANT OPTION; GRANT SELECT ON Stu.CV TO 'cu2'@'localhost' WITH GRANT OPTION; GRANT SELECT, UPDATE ON Stu.CV TO 'cu3'@'localhost' WITH GRANT OPTION;

WITH GRANT OPTION意味着这个用户能把权限再转授给别人,学生账号cu2拿到这个选项后,理论上可以把自己的查询权限授给任意账号,这是权限扩散的隐患。最小授权原则下,只有管理员需要转授权限,学生和教师都不该有。改成:

-- 管理员:全库管理,可转授 GRANT ALL ON Stu.* TO 'admin_u'@'localhost' WITH GRANT OPTION; -- 学生:只能查自己的成绩,通过视图限制行 CREATE VIEW v_my_score AS SELECT * FROM v_score_detail WHERE sno = SUBSTRING_INDEX(USER(), '@', 1); GRANT SELECT ON Stu.v_my_score TO 'stu_u'@'localhost'; -- 教师:查和改成绩,不给删 GRANT SELECT, UPDATE ON Stu.cv TO 'tea_u'@'localhost'; GRANT SELECT ON Stu.v_score_detail TO 'tea_u'@'localhost';

这里用SUBSTRING_INDEX(USER(), '@', 1)把登录用户名当学号做行级过滤,是一种轻量做法,前提是数据库用户名和学号一致。更规范的是应用层拼WHERE sno = ?,但课程设计里用视图 +USER()能演示"同一张表不同用户看到不同行"的效果,答辩时是个加分点。注意GRANT之后要FLUSH PRIVILEGES;才生效,报告没写这句,新手容易卡在"授权了怎么还连不上"。

密码存储那块报告用了MD5和SHA1双写,这两个算法现在都不安全,碰撞成本极低。课程设计可以保留演示,但要知道真实项目该用bcrypt或SHA2加盐,MySQL 5.6 自带SHA2()函数,改一行就行。

4.2 事务里那个 sleep(20) 到底在演示什么

报告的事务设计部分有两个存储过程BB和BC,中间都插了DO SLEEP(20),注释写"暂停 20 秒"。很多人看不懂这行干嘛的,其实它是为了制造一个时间窗口,让你在另一个会话里观察未提交事务的隔离效果:会话 A 开始事务改了学分,睡 20 秒还没 commit,此时会话 B 去查,在REPEATABLE READ隔离级别下读到的是旧值(快照读),在READ COMMITTED下读到旧值直到 A 提交。这个 sleep 是教学演示用的,生产环境绝对不能留,一个请求卡 20 秒,连接池很快被打满。

看BC修改成绩的事务骨架:

DELIMITER // CREATE PROCEDURE sp_update_score_tx( IN p_sno CHAR(10), IN p_cno INT, IN p_result DECIMAL(5,2) ) BEGIN DECLARE t_err INT DEFAULT 0; DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET t_err = 1; START TRANSACTION; UPDATE cv SET result = p_result WHERE sno = p_sno AND cno = p_cno; IF t_err = 1 THEN ROLLBACK; ELSE COMMIT; END IF; END // DELIMITER ;

DECLARE CONTINUE HANDLER FOR SQLEXCEPTION捕获任何 SQL 异常,把标志位置 1,然后根据标志决定回滚还是提交。这个模式是 MySQL 存储过程里做错误处理的标配,比EXIT HANDLER好在过程不会中断,能走到判断分支。但有个坑:CONTINUE HANDLER捕获异常后,出错的语句本身不生效,后续语句继续执行,如果后面还有依赖前一步结果的逻辑,可能基于错误状态继续跑。所以事务里语句越少越好,像报告那样在事务里塞一堆SELECT ... 修改前的查询,一旦某个 SELECT 报错,标志位变了但事务还在,逻辑就乱了。我一般把查询放事务外,事务里只留写操作。

验证事务是否生效,开两个 Navicat 窗口:A 窗口START TRANSACTION; UPDATE cv SET result=90 WHERE sno='001' AND cno=1;不提交,B 窗口SELECT result FROM cv WHERE sno='001' AND cno=1;看读到的是不是旧值,然后 A 提交,B 再查变新值,这一套走完才算真理解隔离级别。

5. 避坑清单:这份实验报告里最容易翻车的五个地方

5.1 现象:建表报Specified key was too long

原因:utf8字符集下索引长度按 3 字节算,VARCHAR(255)建索引就超 767 字节限制,5.6 默认行格式还是COMPACT。解决:建表时加ROW_FORMAT=DYNAMIC,或者把索引字段长度压到VARCHAR(191)以内,字符集统一utf8mb4时更要注意,一个字符 4 字节,191 是安全线。

5.2 现象:存储过程创建成功但调用报No data - zero rows fetched

原因:过程里用了SELECT ... INTO但查询结果为空,没配NOT FOUND处理器。解决:加DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;,或者改用INSERT ... SELECT避免空结果。成绩系统里查一个没选课的学生学分时必现。

5.3 现象:触发器删课程后,CV 表数据没删干净

原因:触发器是AFTER DELETE,如果course表本身有外键约束阻止删除,触发器根本不触发;或者 CV 表有外键指向 course 且没设ON DELETE CASCADE,删除直接报错。解决:先SHOW TRIGGERS;确认触发器状态,再检查information_schema.REFERENTIAL_CONSTRAINTS看外键规则,级联删除要么全交给外键CASCADE,要么全交给触发器,别两套机制混用。

5.4 现象:GRANT执行成功但用户还是连不上

原因:'cu1'@'localhost'和'cu1'@'%'是两个不同账号,应用从别的机器连过来匹配的是%那条,权限没授到。解决:SELECT user, host FROM mysql.user;看清楚账号的 host 部分,按实际连接来源授权,改完FLUSH PRIVILEGES;。

5.5 现象:SUM(result)算出来比手工加的小

原因:result是CHAR(5),里面有空格或空串,隐式转数字变 0,或者某行是NULL被SUM跳过。解决:字段类型改DECIMAL(5,2),查询时SUM(IFNULL(result,0)),并且WHERE result IS NOT NULL先过滤。这个坑最阴,数字看着差不多,对账时才发现差几分。

6. 把实验报告变成可交付脚本:我的验证习惯

一份实验报告和一套能跑的脚本之间,差的是"可重复验证"。我拿到这类文档的第一件事不是通读,而是把里面的 SQL 按依赖顺序抽成一个init.sql:先DROP DATABASE IF EXISTS,再CREATE DATABASE ... DEFAULT CHARSET utf8mb4,然后按 系→专业→班级→学生→教师→课程→CV→ST 的顺序建表,最后统一建视图、存储过程、触发器、用户授权。顺序错了外键就建不上,这个顺序本身就是对 E-R 图理解程度的检验。

抽完脚本跑一遍,重点看三个输出:SHOW TABLES;数一下是不是八张,SHOW TRIGGERS;确认触发器在,SELECT * FROM information_schema.VIEWS WHERE TABLE_SCHEMA='Stu';确认视图全建上了。然后插测试数据,至少造两个班、三个学生、两门课、五条成绩,其中一条成绩故意留NULL,一条学号带前导零,专门用来验证前面说的类型坑。跑一遍v_score_summary看总分平均分对不对,再开两个会话测事务回滚,最后用cu2账号登录执行SELECT * FROM v_my_score;确认只能看到自己的行。

这套流程走下来大概二十分钟,但能提前暴露 90% 的交付问题。从那以后我每次交数据库课设或者给同事 review 建库脚本,都强制先跑一遍这个"建库-插数-查视图-测权限"的闭环,不跑完不签字。这份报告的价值不在它写得多完整,而在于它把 MySQL 从建表到事务的主干知识点串成了一条能动手的线,照着补全、改类型、加索引、收紧权限,就是一套能交差也能复用的成绩库底座。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询