☰
MySQL学生成绩管理系统数据库课设:从E-R图到视图存储过程触发器
2026/10/31 3:09:44 网站建设 项目流程

简介:这份文档是北邮研一数据库课程大作业的完整案例,围绕学生成绩管理系统的设计与实现展开,适合正在完成数据库课程设计、需要参照需求分析与建表规范的高校学生。包内仅含1个docx文件,压缩包约503KB,篇幅集中,便于按章节查阅。内容从功能需求与数据需求入手,给出Course、Student、Sc、Teacher四个表的字段、主键与外键约束,并梳理局部与全局ER图、逻辑结构设计、数据字典、函数依赖集及建库建表SQL。读者可据此理解成绩维护、教师信息管理、不及格名单统计和无教学任务教师查询等功能的落点,也能参考一对多联系与组合主键的处理方式。目前已有607人学习,适合作为课程报告撰写和数据库设计复盘的对照材料。

1. 一个“学生成绩管理系统”大作业,为什么值得拿来拆

每年期末,后台都有一批同学在找数据库课设的参考实现。标题几乎一样:学生成绩管理系统。但真动手时才发现,题目描述都差不多,能跑通的 SQL 脚本和能讲清楚设计思路的文档却很少。这份资源是一个研一数据库大作业的完整交付物,覆盖从需求分析、E-R 图、数据字典到建表脚本、视图、存储过程、触发器、复杂查询的全链路。它不是某个框架的工程代码,而是一套能直接落进 MySQL 执行的关系型数据库设计,适合两类人:一是正在赶课程设计、需要一份结构完整可参考的同学;二是已经工作、想补一补 SQL 基本功和范式设计的开发者。整套东西最实在的地方在于,它把“成绩录入、不及格统计、无教学任务教师查询”这几个真实教务场景,翻译成了四张表和一堆能跑出结果的语句。

2. 数据库设计与技术选型:四张表撑起一个教务场景

2.1 为什么是 MySQL + Navicat 这个组合

原文提到运行环境是 Windows 10 专业版、MySQL 5.5、Navicat for MySQL,语言用 SQL。这套组合在课程设计里非常常见,原因是门槛低、图形化操作直观。作者自己在总结里也说了,一开始用命令行输入,后来换 Navicat 才体会到图形界面在录入和查询上的效率。

提示:MySQL 5.5 是比较老的版本,如果你本地装的是 8.x,建表语法基本兼容,但要注意 8.x 默认字符集是 utf8mb4、默认排序规则不同,导入时如果脚本里写死了字符集可能需要微调。

从技术选型角度看,这个项目没有引入后端语言和 Web 框架,纯靠 SQL 完成全部功能。这对课设来说其实是合理的——题目考查的是数据库设计能力,不是全栈开发。四张表的规模也小,用视图和存储过程就能覆盖“录入、查询、统计”的诉求,不需要额外的应用层。

我一般会把这类纯 SQL 项目分成三层来看:物理层是建库建表和约束,逻辑层是视图、存储过程、触发器,查询层是那些面向业务问题的复杂 JOIN。下面按这个顺序拆。

2.2 实体识别与 E-R 到关系模式的转换

需求里抽象出四个实体:学生、教师、课程、成绩。它们的关系是这样的:一个学生可以选多门课,一门课可以被多个学生选,所以学生和课程是多对多,多对多必须靠一张中间表落地,这就是 sc 成绩表,用 sno 和 cno 做组合主键。教师和课程是一对多,一门课由一个教师讲授,所以 course 表里放了 tno 作为外键指向 teacher。成绩本质上是“学生选某门课的得分”,所以它不是独立实体,而是学生和课程之间关系的属性,挂在 sc 表上。

这个转换过程有个容易翻车的点:题目里 course 表有个字段叫“选课人数(tno)”,长度 char(3),名字叫 tno 但语义是选课人数,这明显是题目描述笔误或者设计上的混淆。实际建表时 tno 就是教师编号,选课人数应该是通过SELECT COUNT(*) FROM sc WHERE cno=?算出来的统计值,不该物理存储。血泪经验是,照着这种有歧义的描述直接建表,后面写查询时会发现字段语义全乱。正确做法是按逻辑结构设计那一节的表结构来,course 表就是 cno、cname、tno 三个字段。

2.3 四张表的字段、类型与约束设计

数据字典这部分是整份资源最值得抄的部分,把每张表的列名、类型、长度、是否允许 NULL、主外键都列清楚了。整理成表更直观:

表名关键字段类型与约束说明
coursecno / cname / tnochar(5) 主键 / varchar(20) 非空 / char(3) 非空课程号、课程名、任课教师号
studentsno / sname / ssex / smajor / sclasschar(9) 主键 / char(8) / char(2) 男或女 / varchar(20) / char(10)学生基本信息
scsno / cno / degreechar(10) 组合主键与外键 / char(5) 组合主键与外键 / decimal(4,1)选课成绩,0-100
teachertno / tname / tsex / tdeptchar(3) 主键 / varchar(8) / char(2) / char(16)教师基本信息与所属院系

这里有几个细节值得注意。degree 用 decimal(4,1),总长度 4 位、小数 1 位,也就是最大能存 999.9,配合 check 约束限制在 0 到 100,够用且精确到 0.1 分,比 float 更适合成绩这种不能有精度误差的场景。sno 在 student 表里是 char(9),但在 sc 表里写成了 char(10),这是个不一致的地方,实际建表时应该统一成 char(9),否则外键关联会报类型不匹配或者隐式转换。这种小坑在课设里很常见,自己写的时候一定要对齐。

3. 建库建表与约束落地:从脚本到可执行

3.1 建库与四张表的完整 DDL

原文给出的建表语句是核心,我按可执行的形式整理并补上了外键和字符集,方便直接跑:

-- 建库,字符集建议显式指定,避免不同环境默认值不一致 CREATE DATABASE IF NOT EXISTS grade_mgmt DEFAULT CHARACTER SET utf8mb4; USE grade_mgmt; -- 教师表:先建,因为 course 要引用它 CREATE TABLE teacher ( tno CHAR(3) NOT NULL, tname VARCHAR(8), tsex CHAR(2), tdept CHAR(16), CONSTRAINT pk_teacher PRIMARY KEY (tno), CONSTRAINT ck_tsex CHECK (tsex IN ('男','女')) ); -- 学生表 CREATE TABLE student ( sno CHAR(9) NOT NULL, sname CHAR(8), ssex CHAR(2), smajor VARCHAR(20), sclass CHAR(10), CONSTRAINT pk_student PRIMARY KEY (sno), CONSTRAINT ck_ssex CHECK (ssex IN ('男','女')) ); -- 课程表:tno 外键指向 teacher,保证任课教师真实存在 CREATE TABLE course ( cno CHAR(5) NOT NULL, cname VARCHAR(20) NOT NULL, tno CHAR(3) NOT NULL, CONSTRAINT pk_course PRIMARY KEY (cno), CONSTRAINT fk_course_teacher FOREIGN KEY (tno) REFERENCES teacher(tno) ); -- 成绩表:组合主键 + 两个外键 + 分数范围约束 CREATE TABLE sc ( sno CHAR(9) NOT NULL, cno CHAR(5) NOT NULL, degree DECIMAL(4,1), CONSTRAINT pk_sc PRIMARY KEY (sno, cno), CONSTRAINT fk_sc_student FOREIGN KEY (sno) REFERENCES student(sno), CONSTRAINT fk_sc_course FOREIGN KEY (cno) REFERENCES course(cno), CONSTRAINT ck_degree CHECK (degree >= 0 AND degree <= 100) );

这段脚本比原文多了几处增强:加了IF NOT EXISTS防止重复执行报错,显式指定了 utf8mb4,补上了外键约束(原文的建表语句里实际上没写外键,只写了主键和 check),还把 sno 统一成了 char(9)。外键这一步很关键——没有外键,sc 表里可能出现一个根本不存在的学号,数据完整性全靠应用层保证,这在纯 SQL 项目里是兜不住的。

参数说明一下:DECIMAL(4,1)表示总共 4 位有效数字、小数点后 1 位;CHECK约束在 MySQL 5.7 以前是被静默忽略的,5.7 之后才真正生效,如果你用 5.5,这个约束不会拦住越界成绩,需要靠触发器或者应用层校验。

3.2 数据插入与类型陷阱

原文给了每张表一条 INSERT 示例,然后说明“其余用 Navicat 添加”。示例语句里有几个值得说的点:

-- course 插入:注意中文字段和引号,原文用的是全角引号,直接跑会报错 INSERT INTO course VALUES ('C01', '科学导论', '101'); -- student 插入 INSERT INTO student VALUES ('120210332', '吴迪', '男', '计算机科学与技术', '4412'); -- sc 插入:degree 是数值,不要加引号 INSERT INTO sc VALUES ('120210332', 'C01', 86.0); -- teacher 插入:注意原文这条示例字段数对不上,teacher 表只有 4 个字段 INSERT INTO teacher VALUES ('140210316', '叶何斌', '男', '计算机 1403');

原文的示例代码里有个明显问题:用的是中文全角引号’,复制到命令行直接执行会报语法错误。另外 teacher 的示例值里塞了“计算机 1403”这种班级信息,但 teacher 表只有 tno、tname、tsex、tdept 四个字段,字段数不匹配,执行会失败。踩坑记录:从文档复制 SQL 时,第一件事是把全角标点替换成半角,第二件事是核对字段数量和顺序。实在懒得写全字段,可以用INSERT INTO teacher(tno,tname,tsex,tdept) VALUES (...)显式列出列名,比裸 VALUES 安全得多。

批量造数据的话,与其一条条 INSERT,不如用存储过程循环插入,几十行学生数据几秒搞定,这也是我在做这类课设时省时间的习惯。

3.3 用视图封装高频查询

视图的作用是把复杂的多表 JOIN 固化成一张“虚拟表”,查询时直接 select 就行。原文建了一个v_student,查所有选修“科学导论”的学生的学号、姓名、成绩:

CREATE VIEW v_student AS SELECT A.sno, A.sname, C.degree FROM student A JOIN sc C ON A.sno = C.sno JOIN course B ON B.cno = C.cno WHERE B.cname = '科学导论'; -- 之后查询直接这样写 SELECT * FROM v_student;

这个视图把三表关联藏起来了,业务方不用关心 sc 和 course 怎么连。逻辑说明:先把 student 和 sc 按学号连起来拿到成绩,再把 course 连进来按课程名过滤。原文还演示了通过视图view_sc往 sc 表插数据、ALTER VIEW 修改、DROP VIEW 删除,这说明可更新视图在基表字段一一对应时是能直接 INSERT 的。注意:一旦视图里带了 JOIN、聚合或者 DISTINCT,就变成只读视图,插入会报错。所以v_student只能查,view_sc这种单表投影才能写。

4. 存储过程、触发器与复杂查询:把业务逻辑下沉到数据库

4.1 两个存储过程的写法与结束符处理

存储过程适合封装“固定步骤 + 参数”的逻辑。原文给了两个:一个按班级查学生,一个统计某学生的选课门数。

DELIMITER // -- 临时把语句结束符改成 //,否则分号会提前截断 CREATE PROCEDURE proc_stud() READS SQL DATA BEGIN SELECT sno, sname, smajor FROM student WHERE sclass LIKE '%4412%' ORDER BY sno; END // DELIMITER ; -- 恢复成默认分号 -- 调用 CALL proc_stud();
DELIMITER // CREATE PROCEDURE num_sc(IN tmp_sno CHAR(9), OUT count_num INT) READS SQL DATA BEGIN SELECT COUNT(*) INTO count_num FROM sc WHERE sno = tmp_sno; END // DELIMITER ; -- 调用:IN 传学号,OUT 接收结果 CALL num_sc('120210332', @cnt); SELECT @cnt;

结束符DELIMITER这块是新手最容易翻车的地方:存储过程体内部有分号,如果不改结束符,MySQL 会在第一个分号处认为语句结束,导致创建失败。逻辑上,proc_stud是纯查询过程,用READS SQL DATA声明它只读、会访问数据;num_sc演示了 IN/OUT 参数,IN 是传入的学号,OUT 是回传的计数,调用时先声明一个用户变量@cnt接。参数类型要和表字段对齐,tmp_sno 用 char(9) 和 student.sno 一致,避免隐式转换导致走不了索引。

4.2 触发器实现删除留痕

需求里有个隐含诉求:学生被删了,历史信息得留一份。原文用触发器实现,把被删学生的学号姓名写进一张备份表。

-- 先照 student 结构建一张空表,WHERE 1=0 保证只复制结构不复制数据 CREATE TABLE delstudent AS SELECT sno, sname FROM student WHERE 1 = 0; DELIMITER // CREATE TRIGGER trig_student AFTER DELETE ON student FOR EACH ROW BEGIN INSERT INTO delstudent(sno, sname) VALUES (OLD.sno, OLD.sname); END // DELIMITER ; -- 验证:删一个人,看备份表有没有记录 DELETE FROM student WHERE sname = '李甜甜'; SELECT * FROM delstudent;

AFTER DELETE表示删除动作完成后再触发,FOR EACH ROW是行级触发器,每删一行执行一次。OLD关键字代表被删除的那行旧数据,能取到它的字段值。这里有个血泪教训:如果 sc 表有指向 student 的外键且没设级联删除,那么删 student 会先被外键拦住报错,触发器根本不会执行。要么先删 sc 里的关联记录,要么给外键加ON DELETE CASCADE。另外CREATE TABLE ... AS SELECT建出来的表不带原表的主键和索引,真要当审计表用,最好手动补上主键。

4.3 面向业务问题的复杂查询

原文这一节的几条 SQL 正好对应需求里的核心业务,值得逐条看:

-- 1. 查所有学生的学号、姓名、性别、专业 SELECT sno, sname, ssex, smajor FROM student; -- 2. 计算 C01 课程平均分 SELECT AVG(degree) FROM sc WHERE cno = 'C01'; -- 3. 查选修“高等数学”的学生学号和姓名 SELECT A.sno, A.sname FROM student A JOIN sc B ON A.sno = B.sno JOIN course C ON B.cno = C.cno WHERE C.cname = '高等数学'; -- 4. 查 C03 课程不及格的学生完整信息 SELECT A.sno, A.sname, A.ssex, A.smajor, A.sclass FROM student A JOIN sc B ON A.sno = B.sno WHERE B.cno = 'C03' AND B.degree < 60;

第 3、4 条是典型的“通过关联表反查主表信息”模式:学生信息在 student,课程信息在 course,两者的连接点只有 sc。写这类查询的关键是先把 JOIN 链路想清楚——student 连 sc 拿学号,sc 连 course 拿课程名,最后在 WHERE 里用课程名或课程号过滤。第 4 条原文多连了一张 course 表其实没必要,已知 cno 直接查 sc 就行,少一次 JOIN 性能更好。这是优化思路:能少连就少连,别为了“看起来完整”堆表。

需求里还有两条没在示例里出现的,我顺手补上:

-- 不及格学生名单(教务处统计用) SELECT C.cname, A.sno, A.sname, B.degree FROM student A JOIN sc B ON A.sno = B.sno JOIN course C ON B.cno = C.cno WHERE B.degree < 60 ORDER BY C.cname, B.degree; -- 没有教学任务的教师(左连接 + IS NULL 的经典用法) SELECT T.tno, T.tname FROM teacher T LEFT JOIN course C ON T.tno = C.tno WHERE C.tno IS NULL;

第二条是这道题里最有含金量的查询,考的是LEFT JOIN ... WHERE 右表主键 IS NULL这个反连接套路,用来找“在 A 表存在但在 B 表不存在”的记录。很多人第一反应是NOT IN,但子查询里有 NULL 值时NOT IN会返回空结果,用 LEFT JOIN 更稳。

5. 避坑与排查:跑这份课设最常见的 5 个问题

5.1 中文乱码:全角引号和字符集双重坑

现象:把文档里的 INSERT 语句复制到命令行,报You have an error in your SQL syntax。原因有两个,一是原文用了中文全角引号’,二是某些环境建库没指定字符集,存中文变问号。解决:执行前全局替换全角标点为半角;建库时显式DEFAULT CHARACTER SET utf8mb4,连接时确认客户端编码一致(SHOW VARIABLES LIKE 'character_set%';看一眼)。

5.2 外键报 1452:插数据的顺序错了

现象:执行INSERT INTO sc VALUES ('120210332','C01',86)时报Cannot add or update a child row。原因:sc 的 sno 和 cno 分别引用了 student 和 course,如果这两张表里还没有对应记录,外键校验过不去。解决:按 teacher → student → course → sc 的顺序插入,父表数据先到位。真要临时跳过校验,可以SET FOREIGN_KEY_CHECKS=0;,但导完数据记得改回 1,不然完整性就是纸糊的。

5.3 CHECK 约束不生效:版本问题

现象:插了一条 120 分的成绩,居然成功了。原因:MySQL 5.7 以前的版本会解析 CHECK 语法但直接忽略,不实际校验。解决:升级到 8.x,或者用 BEFORE INSERT 触发器手动校验并 SIGNAL 报错。这也是为什么我建议用新版本跑这份脚本。

5.4 存储过程创建失败:DELIMITER 忘了改回

现象:执行完建存储过程的脚本后,后面所有语句都报错。原因:DELIMITER //改了结束符,如果中途出错没执行到DELIMITER ;,后续语句的结束符还是//。解决:养成成对书写的习惯,每个 CREATE PROCEDURE 块后面紧跟DELIMITER ;。在 Navicat 里可以直接在查询窗口跑,它会自动处理结束符,比命令行省心。

5.5 删学生触发外键拦截,触发器不执行

现象:执行DELETE FROM student WHERE sname='李甜甜'报外键错误,delstudent 表里啥也没有。原因:sc 表里有该学生的选课记录,外键限制了父表删除。解决:先DELETE FROM sc WHERE sno=...再删 student;或者建外键时加ON DELETE CASCADE,让子表记录跟着级联删除。但级联删除要慎用,成绩数据被连带删掉可能不是你想要的结果,用触发器留痕反而更稳妥。

6. 二次开发与扩展:把课设改成能演示的小系统

6.1 从纯 SQL 到带界面的最小改造路径

这套东西目前是纯 SQL 交付,答辩时只能对着命令行或 Navicat 演示。想加分,最省力的改造是套一个轻量后端。我的习惯是用 Python + Flask 或者 Java + Spring Boot,把每个存储过程和视图挂成一个接口,前端用最简单的 HTML 表单。改造顺序建议是:先把四条核心查询(学生信息、平均分、不及格名单、无任务教师)封装成 REST 接口,再做成绩录入和删除的表单,最后把触发器留痕的 delstudent 表做成一个审计页。这样数据库层一行不用改,工作量集中在接口和页面上,两三天能出雏形。

6.2 索引与性能:小数据量也要养成的习惯

原文“其他考虑”里提到了建索引但没展开。这份数据的量级很小,索引对性能影响不明显,但从工程习惯角度该补:

-- sc 表按课程号查不及格名单很频繁,组合主键已经覆盖 (sno,cno) -- 但单独按 cno 过滤用不到最左前缀,补一个 CREATE INDEX idx_sc_cno_degree ON sc(cno, degree); -- 按课程名查课是高频操作 CREATE INDEX idx_course_cname ON course(cname); -- 按班级查学生 CREATE INDEX idx_student_class ON student(sclass);

逻辑说明:联合索引遵循最左前缀原则,(cno, degree)能同时服务“按课程查”和“按课程查不及格”两种场景。索引不是越多越好,每个索引都会拖慢写入,课设这种读多写少的场景补两三个关键的就行。

6.3 交付前的验证清单

改完之后怎么确认没坏?我会跑一遍固定回归:先SELECT COUNT(*)核对四张表行数,再依次执行四条核心查询看结果是否合理,然后插一条越界成绩确认 CHECK 或触发器拦住,最后删一个学生验证 delstudent 有留痕。这套流程跑通,答辩演示基本不会翻车。

-- 一条语句快速自检各表数据量 SELECT 'student' AS tbl, COUNT(*) AS cnt FROM student UNION ALL SELECT 'teacher', COUNT(*) FROM teacher UNION ALL SELECT 'course', COUNT(*) FROM course UNION ALL SELECT 'sc', COUNT(*) FROM sc;

6.4 一个具体技巧:用反连接替代 NOT IN

前面提过“没有教学任务的教师”这个查询,这里再强调一次,因为它是这份课设里最容易被面试官追问的点。NOT IN在子查询返回 NULL 时会整体失效,而LEFT JOIN ... IS NULL和NOT EXISTS都不受 NULL 影响。我一般首选NOT EXISTS,语义清晰且优化器处理得好:

SELECT tno, tname FROM teacher T WHERE NOT EXISTS ( SELECT 1 FROM course C WHERE C.tno = T.tno );

这个写法在数据量涨上去以后也比 LEFT JOIN 更容易走半连接优化。从那以后我每次写“查不在某集合里的记录”,都会先想一遍 NULL 会不会坏事,能不用 NOT IN 就不用——这个习惯帮我躲过好几次线上统计口径对不上的问题。这份课设虽然小,但把范式、约束、视图、存储过程、触发器、反连接这些点都串了一遍,拿它当 SQL 基本功的练手材料是够用的。希望帮到你。

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

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

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

立即咨询