ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

北邮研一数据库大作业:学生成绩管理系统从设计到优化全流程

北邮研一数据库大作业:学生成绩管理系统从设计到优化全流程 简介这份资源是北邮研一数据库课程大作业的完整详解文档面向正在修读数据库系统课程、需要完成课程设计的研究生及高年级本科生。内容围绕学生成绩管理系统展开覆盖需求分析、数据库设计、ER图绘制、逻辑结构设计与建表程序等完整流程可帮助读者理清从需求到实现的整体思路。压缩包内共1个docx文件约503KB以文字与图表形式呈现设计文档便于直接参考与修改。文档中详细给出Course、Student、Sc、Teacher四张表的结构定义、主外键约束与函数依赖分析并包含不及格学生名单统计、无教学任务教师查询等典型SQL场景还附有建库建表的完整语句适合作为课程报告撰写与数据库建模练习的对照范本。目前已有606人学习下载对需要快速搭建成绩管理系统方案的同学具有较高参考价值。1. 北邮研一数据库大作业从选题到跑通一个学生成绩管理系统的完整落地路径北邮研一的数据库大作业通常不是让你写一篇综述而是要求你从零设计一个能跑起来的小型系统把 ER 图、建表语句、增删改查、事务和索引优化串成一条线。绝大多数人选的题目就是学生成绩管理系统因为它业务闭环清晰学生、课程、教师、选课、成绩五个实体就能撑起一套完整的数据库设计。但真正动手时你会发现卡住你的往往不是 SQL 语法而是环境装不上、外键约束报错、查询慢得离谱这些工程问题。这篇笔记面向正在做这个作业的研一同学也面向任何想用一个真实项目把 MySQL 从安装到优化走一遍的初学者。我会按我当年踩坑的顺序把环境、建模、CRUD、事务、索引、排错全部讲清楚让你能照着复现一套能演示、能答辩、能扛住老师追问的系统。2. 环境选型与建库MySQL 8 加 Navicat 的最小可用组合2.1 为什么是 MySQL 而不是 SQL Server 或 SQLite数据库大作业最常见的三个候选是 MySQL、SQL Server 和 SQLite。SQLite 零配置一个文件就是一个库适合做嵌入式演示但它不支持完整的用户权限体系也没有真正的并发写入能力老师一问“你怎么处理两个学生同时选同一门课”就容易露怯。SQL Server 功能强但安装包动辄几个 GWindows 上还依赖一堆运行库很多同学卡在安装环节就耗掉两天。MySQL 8 是目前高校和互联网公司都用得最多的关系型数据库安装包适中社区资料多Navicat 这类图形化工具对它的支持也最成熟。我的建议是本地开发用 MySQL 8 社区版图形化客户端用 Navicat Premium两者配合能覆盖建表、查询、导入导出、ER 图生成的全部需求。安装 MySQL 时最容易翻车的地方是字符集和认证插件。MySQL 8 默认字符集已经是 utf8mb4但默认认证插件从 mysql_native_password 换成了 caching_sha2_password一些老版本的客户端连不上。如果你用 Navicat 连接时报“Authentication plugin cannot be loaded”说明你的 Navicat 版本太老要么升级 Navicat要么在 MySQL 里把用户认证方式改回去。改法如下-- 查看当前用户的认证插件 SELECT user, host, plugin FROM mysql.user WHERE user root; -- 如果 plugin 是 caching_sha2_password改成 mysql_native_password ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;这段 SQL 先查当前 root 用户用的认证插件如果是 caching_sha2_password 就改成 mysql_native_password最后刷新权限让改动生效。注意密码要换成你自己的改完之后 Navicat 就能正常连接了。这个坑我当年卡了整整一个下午网上搜到的教程大多只讲安装不讲认证插件血泪经验。2.2 建库建表的完整 SQL 与字段设计理由学生成绩管理系统的核心表有五张学生表、课程表、教师表、选课表、成绩表。其中选课表和成绩表可以合并也可以分开我倾向于分开因为选课记录和成绩记录的生命周期不同——选课可能退选成绩一旦录入就不该轻易改。下面是我实际用的建表语句-- 创建数据库指定字符集 CREATE DATABASE student_score DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_score; -- 学生表 CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender ENUM(男,女) DEFAULT 男 COMMENT 性别, major VARCHAR(50) COMMENT 专业, grade_year INT COMMENT 入学年份, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) COMMENT 学生信息表; -- 教师表 CREATE TABLE teacher ( teacher_id VARCHAR(20) PRIMARY KEY COMMENT 工号, name VARCHAR(50) NOT NULL COMMENT 姓名, title VARCHAR(30) COMMENT 职称, department VARCHAR(50) COMMENT 院系 ) COMMENT 教师信息表; -- 课程表 CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY COMMENT 课程号, course_name VARCHAR(100) NOT NULL COMMENT 课程名, credit DECIMAL(3,1) DEFAULT 0.0 COMMENT 学分, teacher_id VARCHAR(20) COMMENT 授课教师工号, semester VARCHAR(20) COMMENT 开课学期, FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ) COMMENT 课程信息表; -- 选课表 CREATE TABLE enrollment ( enroll_id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, course_id VARCHAR(20) NOT NULL, enroll_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) ) COMMENT 选课记录表; -- 成绩表 CREATE TABLE score ( score_id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, course_id VARCHAR(20) NOT NULL, score DECIMAL(5,2) DEFAULT 0.00 COMMENT 成绩, exam_type VARCHAR(20) DEFAULT 期末 COMMENT 考试类型, UNIQUE KEY uk_student_course_type (student_id, course_id, exam_type), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) ) COMMENT 成绩记录表;这里有几个设计决策值得说明。学号用 VARCHAR 而不是 INT因为学号可能有前导零而且不同学校的学号规则不同用字符串更通用。成绩用 DECIMAL(5,2) 而不是 FLOAT因为浮点数在比较和求和时会有精度问题DECIMAL 是精确小数适合分数这种场景。选课表上加了 UNIQUE KEY (student_id, course_id)这是为了防止同一个学生重复选同一门课数据库层面的约束比应用层判断更可靠。成绩表上加了 UNIQUE KEY (student_id, course_id, exam_type)允许同一门课有多次考试成绩但同一类型只能有一条记录。注意外键约束在批量导入数据时会拖慢速度如果你要导入几千条测试数据可以先 SET FOREIGN_KEY_CHECKS 0导入完再设回 1。但正式演示时一定要保持外键开启这是老师检查数据完整性的重点。3. 增删改查与事务把业务逻辑写进 SQL 里3.1 学生成绩管理系统的核心 CRUD 语句增删改查是数据库大作业的基本盘但很多同学只写了最简单的 INSERT 和 SELECT没有体现业务逻辑。我建议至少把下面这几类查询写进你的系统答辩时能直接展示-- 1. 插入学生 INSERT INTO student (student_id, name, gender, major, grade_year) VALUES (20240101, 张三, 男, 计算机科学与技术, 2024); -- 2. 查询某学生所有课程成绩带课程名和教师名 SELECT s.name AS 学生姓名, c.course_name AS 课程名, t.name AS 教师, sc.score AS 成绩, c.credit AS 学分 FROM student s JOIN score sc ON s.student_id sc.student_id JOIN course c ON sc.course_id c.course_id JOIN teacher t ON c.teacher_id t.teacher_id WHERE s.student_id 20240101 ORDER BY c.semester, c.course_id; -- 3. 查询每门课的平均分、最高分、最低分 SELECT c.course_name, COUNT(*) AS 选课人数, AVG(sc.score) AS 平均分, MAX(sc.score) AS 最高分, MIN(sc.score) AS 最低分 FROM score sc JOIN course c ON sc.course_id c.course_id GROUP BY c.course_id, c.course_name HAVING COUNT(*) 1 ORDER BY 平均分 DESC; -- 4. 更新成绩 UPDATE score SET score 95.5 WHERE student_id 20240101 AND course_id CS101 AND exam_type 期末; -- 5. 删除选课记录同时要删成绩用事务保证一致性 START TRANSACTION; DELETE FROM score WHERE student_id 20240101 AND course_id CS101; DELETE FROM enrollment WHERE student_id 20240101 AND course_id CS101; COMMIT;第 2 条查询用了三个 JOIN把学生、成绩、课程、教师四张表串起来这是成绩管理系统最典型的查询模式。第 3 条用了 GROUP BY 和聚合函数注意 HAVING 和 WHERE 的区别WHERE 在分组前过滤行HAVING 在分组后过滤组。第 5 条用事务包住两个 DELETE因为选课记录和成绩记录必须同时删除否则会出现有成绩没选课的脏数据。事务的 ACID 特性在这里体现得很直接要么两条都成功要么两条都回滚。3.2 事务隔离级别与并发选课的处理数据库大作业如果只做单用户演示事务隔离级别用默认的 REPEATABLE READ 就够了。但如果你想在答辩时加分可以演示一下并发场景。比如两个学生同时选同一门课而课程容量有限这时候就需要用事务加锁来保证不超选。下面是一个简化的选课事务-- 开启事务 START TRANSACTION; -- 锁定课程行防止其他事务同时修改 SELECT remaining_seats FROM course WHERE course_id CS101 FOR UPDATE; -- 检查余量假设余量为 1 -- 如果余量大于 0插入选课记录并扣减余量 INSERT INTO enrollment (student_id, course_id) VALUES (20240102, CS101); UPDATE course SET remaining_seats remaining_seats - 1 WHERE course_id CS101; -- 提交事务 COMMIT;这里的关键是 SELECT ... FOR UPDATE它会对查询到的行加排他锁其他事务再想锁这一行就得等待。这样就能保证检查余量和扣减余量之间不会被其他事务插入。注意 FOR UPDATE 必须在事务里用否则锁会立即释放。另外如果并发量很大这种悲观锁的方式会导致大量等待实际生产环境可能会用乐观锁或者队列但大作业演示到这个程度已经足够了。提示MySQL 默认的隔离级别是 REPEATABLE READ你可以用 SELECT transaction_isolation; 查看。如果想演示脏读或不可重复读可以临时把隔离级别改成 READ UNCOMMITTED但演示完记得改回来。4. 索引与慢查询让成绩统计从 3 秒降到 0.1 秒4.1 什么时候该建索引什么时候不该建索引是数据库大作业里最容易拿分也最容易翻车的部分。拿分是因为老师通常会把“有没有做查询优化”作为评分点翻车是因为很多同学不管三七二十一给每个字段都建索引结果插入数据慢得像蜗牛。索引的本质是用空间换时间它加速查询但拖慢写入所以只应该在经常出现在 WHERE、JOIN ON、ORDER BY 里的字段上建。在学生成绩管理系统里我一般会建这几个索引-- 学生表按姓名查询 CREATE INDEX idx_student_name ON student(name); -- 成绩表按学生和课程查询联合索引 CREATE INDEX idx_score_student_course ON score(student_id, course_id); -- 选课表按课程查询 CREATE INDEX idx_enrollment_course ON enrollment(course_id); -- 课程表按教师查询 CREATE INDEX idx_course_teacher ON course(teacher_id);联合索引 idx_score_student_course 的顺序很重要。它先按 student_id 排序再按 course_id 排序所以 WHERE student_id xxx 能用上WHERE student_id xxx AND course_id yyy 也能用上但 WHERE course_id yyy 单独用不上。这就是最左前缀原则。如果你不确定某个查询能不能用上索引可以在 SELECT 前面加 EXPLAIN看 key 列是不是你建的索引名。4.2 用 EXPLAIN 定位慢查询的实操步骤假设你写了一个统计每个学生总学分的查询发现它跑得很慢可以这样排查-- 先看执行计划 EXPLAIN SELECT s.student_id, s.name, SUM(c.credit) AS total_credit FROM student s JOIN score sc ON s.student_id sc.student_id JOIN course c ON sc.course_id c.course_id WHERE sc.score 60 GROUP BY s.student_id, s.name;EXPLAIN 的输出里重点看几列type 表示访问类型ALL 是全表扫描ref 或 eq_ref 是索引查找range 是范围扫描key 表示实际用到的索引rows 表示预估扫描行数。如果 type 是 ALL 且 rows 很大说明没走索引。这时候你可以考虑在 sc.score 上建索引或者调整查询写法。但注意如果 WHERE sc.score 60 筛选出来的行占全表大部分MySQL 可能觉得全表扫描更快这时候建索引反而没用。索引不是万能的数据量小的时候全表扫描可能更快。另一个常见的慢查询是深分页。比如 LIMIT 10000, 20MySQL 会先扫描前 10020 行再丢掉前 10000 行。优化方式是用子查询先定位主键-- 慢查询 SELECT * FROM score ORDER BY score_id LIMIT 10000, 20; -- 优化后 SELECT * FROM score WHERE score_id (SELECT score_id FROM score ORDER BY score_id LIMIT 10000, 1) ORDER BY score_id LIMIT 20;优化后的写法先用子查询拿到第 10000 行的 score_id然后直接从这个 id 之后取 20 行避免了扫描前 10000 行。这个技巧在数据量大时效果非常明显我实测过从 3 秒降到 0.1 秒。5. 避坑与排查那些让大作业卡住的真实问题5.1 外键约束报错 1452 的三种原因现象插入选课记录时报 “Cannot add or update a child row: a foreign key constraint fails”。原因一学生表里没有这个学号。外键要求子表的值必须在父表里存在所以插入选课记录前必须先插入学生。原因二字符集不一致。如果学生表的 student_id 是 utf8mb4而选课表的 student_id 是 latin1即使值看起来一样也会报错。原因三父表被删过数据但子表还有引用。解决方法是先查父表有没有对应记录再检查两张表的字符集和排序规则是否一致最后确认删除顺序是先删子表再删父表。5.2 Navicat 导入 CSV 时中文乱码现象用 Navicat 的导入向导把 CSV 导进 MySQL中文变成问号或乱码。原因CSV 文件的编码是 GBK而 MySQL 表的字符集是 utf8mb4导入时没有指定编码转换。解决方法是在导入向导的“编码”选项里手动选择 GBK或者先用记事本把 CSV 另存为 UTF-8 格式再导入。更稳妥的做法是建表时就指定 utf8mb4导入时也选 utf8mb4两边一致就不会乱码。5.3 事务没提交导致数据“消失”现象在命令行里 INSERT 了一条数据Navicat 里查不到或者程序里插入成功但重启后数据没了。原因MySQL 默认 autocommit 1每条 SQL 自动提交。但如果你手动 START TRANSACTION 之后忘了 COMMIT数据就一直在事务里没落盘。另一个可能是你连的是不同的数据库实例比如命令行连的是本地 3306Navicat 连的是另一个端口。解决方法是执行 COMMIT 或 ROLLBACK 结束事务并用 SELECT autocommit; 确认自动提交状态。5.4 慢 SQL 优化时索引建了但没生效现象明明在 score 字段上建了索引但 EXPLAIN 显示 type ALL。原因一查询条件用了函数比如 WHERE ABS(score) 60索引失效。原因二查询条件用了 LIKE %xxx前置百分号让索引失效。原因三联合索引顺序不对查询条件不符合最左前缀。解决方法是把函数移到等号右边把 LIKE %xxx 改成 LIKE xxx%或者调整联合索引的字段顺序。5.5 批量插入数据太慢现象用 INSERT 一条一条插几千条测试数据跑了十几分钟还没完。原因每条 INSERT 都是一次网络往返和一次磁盘写入autocommit 模式下每条都提交事务。解决方法是把多条 INSERT 合并成一条或者用事务包住批量插入START TRANSACTION; INSERT INTO student VALUES (20240101,张三,男,计算机,2024); INSERT INTO student VALUES (20240102,李四,女,软件工程,2024); -- ... 更多 INSERT COMMIT;合并后几千条数据几秒钟就能插完。如果数据量特别大还可以用 LOAD DATA INFILE但大作业一般用不上。6. 进阶技巧用存储过程和视图把答辩演示拉满如果你已经把基本的增删改查跑通了想在答辩时让老师眼前一亮我建议加两个东西一个存储过程做成绩统计一个视图做学生成绩单。存储过程的好处是把复杂逻辑封装在数据库层调用时只需要一个 CALL 语句视图的好处是简化查询让前端不用写复杂的 JOIN。先看存储过程。下面这个存储过程接收学号返回该学生的所有课程成绩和总学分DELIMITER // CREATE PROCEDURE get_student_report(IN p_student_id VARCHAR(20)) BEGIN -- 成绩明细 SELECT c.course_name, c.credit, t.name AS teacher_name, sc.score FROM score sc JOIN course c ON sc.course_id c.course_id JOIN teacher t ON c.teacher_id t.teacher_id WHERE sc.student_id p_student_id ORDER BY c.semester, c.course_id; -- 总学分和平均分 SELECT SUM(c.credit) AS total_credit, AVG(sc.score) AS avg_score FROM score sc JOIN course c ON sc.course_id c.course_id WHERE sc.student_id p_student_id AND sc.score 60; END // DELIMITER ; -- 调用 CALL get_student_report(20240101);DELIMITER // 的作用是临时把语句结束符从分号改成双斜杠因为存储过程内部有分号不改的话 MySQL 会提前结束。存储过程里两条 SELECT 会返回两个结果集Navicat 里能直接看到两个表格。注意总学分只统计及格课程这是常见的学分计算规则。再看视图。视图是一个虚拟表不存数据每次查询时动态生成。下面这个视图把学生、课程、成绩、教师四张表拼成一张宽表CREATE VIEW v_student_score AS SELECT s.student_id, s.name AS student_name, s.major, c.course_id, c.course_name, c.credit, t.name AS teacher_name, sc.score, sc.exam_type FROM student s JOIN score sc ON s.student_id sc.student_id JOIN course c ON sc.course_id c.course_id JOIN teacher t ON c.teacher_id t.teacher_id; -- 用视图查询就像查单表一样简单 SELECT * FROM v_student_score WHERE student_id 20240101; SELECT course_name, AVG(score) FROM v_student_score GROUP BY course_name;视图的最大价值是屏蔽了底层 JOIN 的复杂性前端或者报表工具只需要 SELECT * FROM v_student_score 就能拿到所有信息。但视图也有代价每次查询都要执行底层的 JOIN数据量大时可能比直接写 JOIN 慢。所以视图适合查询频率高但数据量不大的场景比如成绩单展示。最后说一个验证技巧答辩前一定要用真实数据跑一遍所有查询包括边界情况。比如插入一个没有选课的学生看查询会不会返回空插入一个成绩为 NULL 的记录看 AVG 会不会算错删除一个还有成绩记录的学生看外键会不会阻止。这些边界情况老师大概率会问提前准备好答案比现场翻车强。我当年就是没测 NULL 成绩答辩时被问住了后来补了 COALESCE 函数才解决。希望帮到你。本文还有配套的精品资源点击获取
返回列表