
简介这是一份面向高校数据库课程设计的实践资源主题为“某高校学生选课系统”面向正在完成数据库原理及应用课程设计、需要掌握系统分析与数据库设计全流程的学生。压缩包共3个文件1份课程设计报告doc、1个SQL脚本和1个数据库备份bak分别对应设计文档、数据定义与初始数据整体仅802KB轻量易用。资源已获得998人学习。内容围绕课程设计目的、需求分析、数据库设计及数据录入处理展开报告涵盖系统分析、数据模型优化、数据库结构、功能结构以及安全性与完整性要求等关键环节SQL脚本可直接导入数据库查看表结构、视图、存储过程等设计成果bak备份文件则便于还原完整数据库进行对比验证。无论课程设计还是期末项目都能帮助读者少走弯路适合希望参考高分课设范例、快速理解从需求分析到数据库落地全过程的初学者。1. 数据库课程设计遇到学生选课系统先别急着建表数据库课程设计里学生选课系统是出现频率最高也最容易翻车的题目。表面看无非学生、课程、成绩三张表真正按课程设计标准交上去的时候却经常被一个问题问住同一门课只剩最后一个名额两个学生同时选课拿什么保证不会超选。很多同学把 .rar 压缩包里的脚本重新执行一遍就发现外键顺序错了、联合主键没表达出退课记录、成绩字段挂错了表。下面按课程设计通用的流程走从业务规则、ER 模型、MySQL 建库脚本到存储过程选课事务最后给出答辩前自检技巧所有语句在 MySQL 8.0 下可以直接执行。2. 先定业务规则再建模学生选课系统的实体与关系模式课程设计翻车的第一大原因是拿到题干后直接打开 MySQL 建表连业务规则都还没定。学生选课系统至少要考虑选课时间窗口、课程容量、退课截止时间、重修是否允许重复选这几条约束这些规则决定了选课记录表的唯一约束写在哪一列、容量检查放在事务的哪一步、成绩更新会不会产生脏数据。我一直用的顺序是先列用例和业务规则画 ER 图再补关系模式最后才写 CREATE TABLE哪怕时间再赶也不能跳。2.1 用用例反推实体选课记录是关系实体常见做法是列出学生、教师、教学秘书三类角色把每个角色能做的事翻译成用例。学生查课、选课、退课、查成绩教师开课、录入成绩教学秘书审核开课计划和调容量。去掉重复名词后得到的基本实体有学生、教师、课程、开课计划、选课记录其中选课记录是学生和开课计划之间的关系实体不能省略。开课计划必须在实体清单里单独出现因为同一门课程在不同学期、不同班次会有不同容量与上课时间。把课程和开课计划混在一张表里会让字段出现大量重复后续为了调整上课时间还要拆表。课程设计报告里可以直接用下面这样一张表格交代实体、关键属性、主键与关系评阅老师扫一眼就知道数据模型有没有走样。实体名关键属性主键候选与其它实体的关系studentstudent_id, name, major, gradestudent_id与选课记录为 1:Nteacherteacher_id, name, titleteacher_id与开课计划为 1:Ncoursecourse_id, course_name, creditcourse_id与开课计划为 1:Ncourse_scheduleschedule_id, semester, capacityschedule_id与选课记录为 1:Nenrollstudent_id, schedule_id, score, statusenroll_id 或联合主键关联学生和开课计划2.2 选课记录用单列主键还是联合主键关系模式转出来之后第一个容易被追问的设计点是 enroll 表选哪种主键。联合主键 (student_id, schedule_id) 语义上直接对应“一个学生同一班次只能选一次”适合需求简单的作业但后续如果出现补考记录、重修分批、同一学期同一门课不同教学班联合主键反而需要加入更多列。更稳妥的做法是给 enroll 表加一列自增 enroll_id同时加一个唯一约束 uk_enroll(student_id, schedule_id)。这样做既保留了防重能力又让业务表的主键保持稳定写 JOIN 时也能少一层冗余。课程设计报告里把这个决策写清楚老师会认为你真正考虑过数据生命周期。联合主键在 InnoDB 里直接作为聚簇索引长字符串组合会让次级索引变大换用自增主键配合唯一约束代码里也更容易判断某次操作影响的是选课记录本身还是选课记录对应的学生。下面这条查询适合在答辩前做数据自检用于确认有没有重复选课记录。-- 计算出同一个学生重复选同一班次的次数用于自检唯一性 SELECT student_id, schedule_id, COUNT(*) AS duplicate_count FROM enroll GROUP BY student_id, schedule_id HAVING COUNT(*) 1;这段 SQL 的核心是 GROUP BY 后接 HAVING COUNT(*) 1它筛出的每一行都代表一个潜在的重复选课。如果 enroll 表上有联合唯一约束这个查询通常返回空集如果业务层只写了一个 INSERT 而没有唯一约束这条语句就是答辩时最直接的证据。GROUP BY 后面的两列顺序不影响结果集但会影响 MySQL 是否能用上复合索引实际数据量很小时可以不做额外调整。2.3 用第二范式和第三范式检查属性归属建表前把候选表逐张过一遍范式检查能少填很多坑。第二范式处理联合主键下的部分依赖如果 enroll 表使用 (student_id, course_id) 做联合主键同时放了课程名课程名只依赖 course_id不依赖 student_id这就是部分依赖会导致课程改名时必须更新选课表多行。第三范式处理非主属性之间的传递依赖把学院院长姓名放在 student 表里属于传递依赖学院改名或换院长时会产生不一致。实际修改方向也不难判断把只依赖主键一部分的字段拆出去把依赖非主键的字段也拆出去。课程基本信息和教师基本信息各放各的表选课记录里只保留 student_id 和 schedule_id 两个外键。表面上看查询多了一次 JOIN但增删改查的数据一致性维护成本更低课程设计评审时也更愿意给你过。3. 用 MySQL 建库建表跑通学生选课系统的增删改查关系模式定完后进入实现。下面这套脚本以 MySQL 8.0 为准存储引擎全部用 InnoDB字符集用 utf8mb4排序规则用 utf8mb4_unicode_ci。utf8mb4 不是可选项课程名里出现数学符号、人名时老字符集很容易报 Incorrect string value答辩现场改表结构很浪费时间。InnoDB 提供事务和行级锁后面的选课存储过程要靠它保证并发时不会超选这一点在选题理由里也应该写一句。3.1 建库建表脚本外键先建父表再建子表下面是课程设计里最常用的最小建表脚本包含学生、教师、课程、开课计划、选课记录五张表。注意执行顺序先建 student、teacher、course再造 course_schedule最后建 enroll否则外键引用的表还不存在MySQL 会直接报 errno 150。CREATE DATABASE IF NOT EXISTS course_selection DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE course_selection; CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY, name VARCHAR(50) NOT NULL, major VARCHAR(50), grade INT, enroll_year YEAR ) ENGINEInnoDB; CREATE TABLE teacher ( teacher_id VARCHAR(20) PRIMARY KEY, name VARCHAR(50) NOT NULL, title VARCHAR(20) ) ENGINEInnoDB; CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1), course_type VARCHAR(20) ) ENGINEInnoDB; CREATE TABLE course_schedule ( schedule_id INT AUTO_INCREMENT PRIMARY KEY, course_id VARCHAR(20) NOT NULL, teacher_id VARCHAR(20) NOT NULL, semester VARCHAR(20) NOT NULL, capacity INT DEFAULT 60, selected_count INT DEFAULT 0, CONSTRAINT fk_cs_course FOREIGN KEY (course_id) REFERENCES course(course_id) ON UPDATE CASCADE ON DELETE RESTRICT, CONSTRAINT fk_cs_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ON UPDATE CASCADE ON DELETE RESTRICT ) ENGINEInnoDB; CREATE TABLE enroll ( enroll_id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, schedule_id INT NOT NULL, score DECIMAL(5,2) NULL, status ENUM(selected,dropped,finished) DEFAULT selected, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_enroll (student_id, schedule_id), CONSTRAINT fk_enroll_student FOREIGN KEY (student_id) REFERENCES student(student_id) ON UPDATE CASCADE ON DELETE RESTRICT, CONSTRAINT fk_enroll_schedule FOREIGN KEY (schedule_id) REFERENCES course_schedule(schedule_id) ON UPDATE CASCADE ON DELETE RESTRICT ) ENGINEInnoDB;建表脚本要讲清楚三处参数。course_schedule 的 capacity 与 selected_count 都用 INT课程设计阶段默认 60 即可不要用 SMALLINT 加 CHECK 限制因为旧版本 MySQL 对 CHECK 子句不一定执行。enroll 表的 status 用 ENUM 而不是 VARCHAR能够把合法状态限定在 selected、dropped、finished 三种省掉应用层一次数据校验。外键的 ON UPDATE CASCADE 保证学号、课程号变更时关联子表自动同步ON DELETE RESTRICT 防止误删父表记录把成绩历史一起带走这两个选项在答辩时是被问到最多的部分。提示如果脚本要反复执行表已存在时先 DROP 再跑或者在建表语句上补 IF NOT EXISTS不要图方便开 SET FOREIGN_KEY_CHECKS0否则外键约束在导入数据时会失效重复执行脚本很可能留下孤立数据。3.2 插入测试数据并用 JOIN 完成典型查询脚本建好以后先插入几条带关联关系的测试数据再跑查询完整验证外键是否生效。课程设计报告中这一步对应“系统测试”或“功能验证”章节。INSERT INTO student (student_id, name, major, grade, enroll_year) VALUES (2023001, 张三, 计算机科学与技术, 2023, 2023), (2023002, 李四, 软件工程, 2023, 2023); INSERT INTO course (course_id, course_name, credit, course_type) VALUES (CS101, 数据库原理, 3.5, 专业必修), (CS102, 操作系统, 3.0, 专业必修); INSERT INTO teacher (teacher_id, name, title) VALUES (T001, 王老师, 副教授), (T002, 陈老师, 讲师); INSERT INTO course_schedule (course_id, teacher_id, semester, capacity) VALUES (CS101, T001, 2024-2025-1, 60), (CS102, T002, 2024-2025-1, 60); INSERT INTO enroll (student_id, schedule_id) VALUES (2023001, 1), (2023002, 1);插入语句的要点是多值 INSERT比逐条 VALUES 短也便于恢复测试。注意 enroll 表没有显式插入 status 和 enroll_idstatus 取默认值 selectedenroll_id 由自增列生成。真正要判断的外键是 student_id 和 schedule_id如果插入一个不存在的学生MySQL 会报 foreign key constraint fails如果插入一个不存在的 schedule_id也是外键约束报错这个错误信息对课程设计排错很有用。典型查询是“查看某学生已选课程列表”它需要把 student、enroll、course_schedule、course 四张表串起来。SELECT s.student_id, s.name, c.course_name, cs.semester, e.status, e.score FROM enroll e JOIN student s ON e.student_id s.student_id JOIN course_schedule cs ON e.schedule_id cs.schedule_id JOIN course c ON cs.course_id c.course_id WHERE s.student_id 2023001;这个查询用三个 JOIN 完成多表关联顺序上一般从事实表 enroll 出发再逐级补充学生、开课计划、课程信息。WHERE 比 JOIN ON 晚一步过滤所以当数据量变大时尽量在 JOIN 之前用子查询先缩小 student 集合课程设计阶段可以不做优化但要能说清楚执行计划会怎么走。3.3 用 ALTER TABLE 调整结构UPDATE 和 DELETE 各补一例课程设计报告里经常要体现“数据库维护功能”至少要有一次 UPDATE 和一次 DELETE。修改课程名称对应 UPDATE退课记录对应 DELETE。UPDATE course SET course_name 数据库系统概论 WHERE course_id CS101; DELETE FROM enroll WHERE student_id 2023001 AND schedule_id 1; SELECT ROW_COUNT() AS affected_rows;UPDATE 的 WHERE 条件必须带主键或唯一索引否则会整表更新。DELETE 同理课程设计阶段没有开事务时漏掉 WHERE 会把整张表清空。SELECT ROW_COUNT() 返回上一条语句影响的行数可以用来确认到底删掉了哪一行。如果执行完发现数据乱了最简单的方式是 DROP DATABASE course_selection然后重新执行 3.1 的建表脚本和 3.2 的插入脚本不要在半路手动改数据。说到结构变更常用的是 ALTER TABLE。给课程表增加校区字段、修改容量默认值这两条在课程设计中足够覆盖大部分评分点。ALTER TABLE course ADD COLUMN campus VARCHAR(50) DEFAULT 东校区; ALTER TABLE course_schedule MODIFY COLUMN capacity INT NOT NULL DEFAULT 80;ALTER TABLE 在执行时会与 DML 语句产生元数据锁竞争但在课程设计的小数据量下基本无感。MODIFY COLUMN 会重建表因此尽量把容量默认值和类型一次改到位不要分两次操作。4. 用存储过程实现选课事务解决并发选课和容量检查学生选课系统的评分拉开差距的地方通常在并发控制。普通做法是先 SELECT selected_count 判断是否满员再 INSERT但这个顺序在两个会话同时执行时会出现同一时间读到同一个人数最后两个人都插入成功课程超容。解决办法是把判断和插入放进同一个事务并用 FOR UPDATE 锁住开课计划行这个思路在数据库课程设计的答辩里要能完整复述。4.1 为什么选课逻辑要收进存储过程而不是写三条业务 SQL如果应用层先执行 SELECT再执行 INSERT中间一旦有网络抖动或页面重复提交两次请求根本不会感知对方。存储过程的优势是数据库端就能保证原子性应用层只要调用一个 CALL 语句。课程设计中使用存储过程的另一个原因是报告上可以写“使用了数据库端业务逻辑封装”这是一个明确的加分点代价是调试比普通 SQL 麻烦存储过程内部变量无法像应用日志那样直接打印。4.2 选课存储过程的完整脚本与参数说明下面是一个带事务和行锁的选课存储过程用 InnoDB 的行级锁串行化同一个 schedule 的选课请求。DELIMITER $$ CREATE PROCEDURE sp_select_course( IN p_student_id VARCHAR(20), IN p_schedule_id INT ) BEGIN DECLARE v_capacity INT DEFAULT 0; DECLARE v_selected INT DEFAULT 0; DECLARE v_exists INT DEFAULT 0; START TRANSACTION; SELECT capacity, selected_count INTO v_capacity, v_selected FROM course_schedule WHERE schedule_id p_schedule_id FOR UPDATE; SELECT COUNT(*) INTO v_exists FROM enroll WHERE student_id p_student_id AND schedule_id p_schedule_id; IF v_exists 0 OR v_selected v_capacity THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT repeat selection or course is full; ELSE INSERT INTO enroll (student_id, schedule_id, status, select_time) VALUES (p_student_id, p_schedule_id, selected, NOW()); UPDATE course_schedule SET selected_count selected_count 1 WHERE schedule_id p_schedule_id; COMMIT; END IF; END$$ DELIMITER ;参数说明p_student_id 和 p_schedule_id 是入参分别代表学号和开课计划号v_capacity、v_selected、v_exists 是局部变量必须声明在 BEGIN 之后、任何可执行语句之前。SELECT ... FOR UPDATE 是这一脚本的关键它会把 course_schedule 表对应 schedule_id 的行加上排他锁其他事务更新同一行时会阻塞直到 COMMIT 或 ROLLBACK。SELECT COUNT(*) INTO v_exists 用来检查重复选课即使外层的 uk_enroll 唯一约束已经存在事务里也要再查一次这样做是为了给出可读的报错信息而不是数据库抛一个含糊的 duplicate key。IF 分支里只要满足重复选课或人数已满就执行 ROLLBACK 并用 SIGNAL 抛错。SIGNAL SQLSTATE 45000 是 MySQL 手动触发异常的标准写法SET MESSAGE_TEXT 的内容会返回给客户端应用层可以捕获后提示用户。正常情况下执行 INSERT 和 UPDATECOMMIT 统一提交。需要注意 INSERT 里显式写了 select_time调用时传入 NOW()这样选课时间由数据库服务器统一生成避免应用服务器时间不同步。4.3 模拟两个客户端并发选课观察锁等待行为验证存储过程最直接的方法是开两个 mysql 客户端同时向同一个 schedule_id 发起选课。会话 A 先执行 START TRANSACTION然后调用过程暂时不提交-- 会话 A START TRANSACTION; CALL sp_select_course(2023001, 1); -- 此时不 COMMIT行锁被 A 持有 -- 会话 B CALL sp_select_course(2023002, 1); -- B 会阻塞在 SELECT ... FOR UPDATE直到 A 提交或超时如果 A 的选课结果是先进来占最后一个名额B 会一直等待等到 A COMMIT 后才会读到新的 selected_count然后看到容量已满而回滚。这个现象解释了为什么 FOR UPDATE 比普通 SELECT 安全。生产环境中会在连接池层设置超时时间MySQL 侧对应参数是 innodb_lock_wait_timeout默认 50 秒课程设计里可以调成 5 秒方便演示SET SESSION innodb_lock_wait_timeout 5; CALL sp_select_course(2023002, 1);设置超时后如果 B 等待超过 5 秒MySQL 会返回 Lock wait timeout exceeded不会无限挂住。答辩演示时最好把两个终端窗口同时截进录屏先开一个选课成功再开另一个显示报错比口头解释事务隔离级别更有说服力。还有一种情况是死锁A 选了课程 1 又选课程 2B 选了课程 2 又选课程 1互相持有行锁InnoDB 会自动检测并回滚其中一个事务。实际业务里避免死锁的常见做法是统一选课顺序比如总是按 schedule_id 从小到大处理。5. 交 .rar 之前用两条 SQL 自查顺手把报告内容固化课程设计最终提交的一般是一个压缩包里面放着报告、SQL 脚本和截图。老师打开交付包后的第一步通常是在干净数据库里执行一遍你的脚本这一步很容易出问题。常见现象是脚本里的建表顺序和外键交叉混乱或者某张表里的字符集不对导致中文变问号。我一般会固定一个最小自查顺序跑完两条 SQL 再打包 .rar。5.1 用 information_schema 确认表和行数SELECT table_name, table_rows, auto_increment FROM information_schema.tables WHERE table_schema course_selection;这条语句把表名、估算行数、自增偏移量一起列出。要注意 information_schema.table_rows 是优化器估算值不是精确行数InnoDB 尤其只是近似值。把它拿来对比各表是否都有数据适合作为脚本是否执行完整的快速判断。auto_increment 字段可以检查自增主键有没有因为多次删除被推得很高如需复位通常用 ALTER TABLE ... AUTO_INCREMENT 1 完成。5.2 用 LEFT JOIN 反查外键孤立行拿到一个别人给的脚本时如果对方在执行过程中临时关了外键检查常见结果是 enroll 表里出现 student 表中不存在的学号左边 JOIN 一下就能看出来。课程设计里自己的脚本最好也要跑一遍保证交付的建表脚本不会产生游离数据。SELECT e.enroll_id, e.student_id, e.schedule_id FROM enroll e LEFT JOIN student s ON e.student_id s.student_id WHERE s.student_id IS NULL UNION ALL SELECT e.enroll_id, e.student_id, e.schedule_id FROM enroll e LEFT JOIN course_schedule cs ON e.schedule_id cs.schedule_id WHERE cs.schedule_id IS NULL;两条查询分别检测所选学生是否存在于 student 表以及开课计划是否存在于 course_schedule 表只要查询结果返回 0 行说明外键关系完整。课程设计报告里放这个结果截图比放一堆功能页面截图更能说明你理解了关系完整性。如果发现孤立行优先检查外部导入脚本里的 SET FOREIGN_KEY_CHECKS0 是否在结尾被恢复成 1。再顺手处理交付压缩包的结构。我一般会把 SQL 文件拆成 01_schema.sql、02_data.sql、03_procedure.sql 三个编号文件每个文件开头都写上 USE course_selection; 和 SET NAMES utf8mb4;防止用 Navicat 导入时选错库或出现中文乱码。最后在 Navicat 里用“转储 SQL 文件”并勾选“包含建库语句”把导出的脚本连同报告、ER 图截图一起压进 .rar文件名带上课程号和学号就能直接提交。本文还有配套的精品资源点击获取