简介:这份资源是面向计算机相关专业在校学生的SQL Server学生选课系统数据库设计课程设计包,适合作为期末大作业、课设答辩或项目初期立项的参考模板,也便于初学者理解数据库建模与SQL编程的完整流程。压缩包共6个文件,约139KB,包含sql建库建表脚本、docx详细设计文档、md说明文件以及png结构示意图,另附一份zip源码,覆盖从需求分析到表结构落地的关键环节。目前已有441人学习下载,说明其内容具备一定参考价值。读者可从中获取完整的选课系统数据库设计方案,包括实体关系梳理、数据表字段定义、约束与索引设置思路,以及可直接运行的SQL脚本,便于对照文档理解设计意图,也能在此基础上修改扩展为其他教务管理功能,适合需要快速完成课设或学习数据库设计规范的同学使用。
1. 学生选课系统数据库设计:从建表到选课冲突,一套能交课程设计的完整方案
每年学期末,总有一批人对着「数据库课程设计」六个字发愁。题目发下来是「学生选课系统」,要求写文档、建库、写源码、做答辩,但真动手时才发现:表该怎么拆、选课冲突怎么防、容量满了怎么处理,这些课本上一笔带过的东西,恰恰是答辩老师最爱追问的地方。这份基于 SQL Server 的学生选课系统数据库设计,要解决的就是从 ER 图到可运行脚本这一整条链路——它不是让你背范式,而是让你交出一套能跑、能查、能讲清楚设计取舍的东西。适合正在做课程设计的学生,也适合想拿一个完整案例练手 SQL Server 建库、约束、存储过程和事务的开发者。下面按「先立设计、再落脚本、最后排坑」的顺序讲透。
2. 先把表拆对:学生选课系统的实体识别与关系建模
2.1 从业务动作反推实体,而不是从课本抄 ER 图
很多人一上来就画 ER 图,结果画完发现字段对不上业务。我的习惯是先把业务动作列出来:学生入学建档、教师开课、学生选课、退课、录入成绩、统计学分。每个动作背后至少有一个实体在承载数据。
- 学生入学建档 → 学生表(Student)
- 教师开课 → 教师表(Teacher)+ 课程表(Course)+ 开课表(CourseOffering)
- 学生选课/退课 → 选课表(Enrollment)
- 录入成绩 → 成绩字段挂在选课表上,而不是单独建表
- 统计学分 → 课程表里存学分,选课表里存是否通过
这里最容易翻车的是把「课程」和「开课」混成一张表。课程是「数据结构,4 学分,专业必修」,开课是「2024 秋季,张老师,周三 3-4 节,容量 60 人」。两者是一对多,必须拆开。不拆的后果是:同一门课不同学期开,你得复制一堆课程信息,改一个学分要改几十行。
2.2 五张核心表 + 两张字典表的结构设计
下面是我一般会用的表结构,字段名用英文,注释写清楚,方便后面写文档直接贴。
| 表名 | 中文名 | 关键字段 | 说明 |
|---|---|---|---|
| Student | 学生表 | StudentID, Name, Gender, MajorID, Grade | 学号做主键 |
| Teacher | 教师表 | TeacherID, Name, Title, DeptID | 工号做主键 |
| Course | 课程表 | CourseID, CourseName, Credit, CourseType | 课程编号做主键 |
| CourseOffering | 开课表 | OfferingID, CourseID, TeacherID, Semester, Capacity, Enrolled | 自增主键,外键关联课程和教师 |
| Enrollment | 选课表 | EnrollmentID, StudentID, OfferingID, SelectTime, Score | 联合唯一约束防重复选课 |
| Department | 院系表 | DeptID, DeptName | 字典表 |
| Major | 专业表 | MajorID, MajorName, DeptID | 字典表 |
选课表上的(StudentID, OfferingID)要加唯一约束,这是防重复选课的第一道防线。成绩字段允许为空,表示还没录入。Enrolled字段是已选人数,用来和Capacity比较判断是否满员。
2.3 主键、外键、唯一约束该怎么定
主键选择上,学生表用学号、教师表用工号、课程表用课程编号,这些都是业务主键,稳定且唯一。开课表和选课表用自增 ID,因为它们的业务键可能变化(比如开课记录调整),自增 ID 更省心。
外键方面,选课表的StudentID引用学生表,OfferingID引用开课表,开课表的CourseID引用课程表、TeacherID引用教师表。外键的作用不只是约束,还能在写多表联查时帮你理清关系。
唯一约束除了选课表的联合唯一,还有学生表的学号、教师表的工号、课程表的课程编号,这些在建表时直接加 UNIQUE 即可。
提示:外键要不要加 ON DELETE CASCADE 要慎重。学生退学删学生记录时,如果级联删选课记录,历史成绩就没了。我一般不加级联,改用软删除或状态字段。
3. 建库建表脚本:一份能直接跑的 SQL Server 源码
3.1 建库与建表的完整 T-SQL 脚本
下面这段脚本可以直接在 SSMS 里执行,建库、建表、加约束一次到位。注意 SQL Server 的IDENTITY(1,1)是自增,NVARCHAR存中文更稳妥。
-- 建库 IF DB_ID('StudentCourseDB') IS NULL CREATE DATABASE StudentCourseDB; GO USE StudentCourseDB; GO -- 院系表 CREATE TABLE Department ( DeptID INT PRIMARY KEY, DeptName NVARCHAR(50) NOT NULL ); -- 专业表 CREATE TABLE Major ( MajorID INT PRIMARY KEY, MajorName NVARCHAR(50) NOT NULL, DeptID INT NOT NULL, CONSTRAINT FK_Major_Dept FOREIGN KEY (DeptID) REFERENCES Department(DeptID) ); -- 学生表 CREATE TABLE Student ( StudentID CHAR(10) PRIMARY KEY, Name NVARCHAR(20) NOT NULL, Gender CHAR(2) CHECK (Gender IN ('男','女')), MajorID INT NOT NULL, Grade INT NOT NULL, CONSTRAINT FK_Student_Major FOREIGN KEY (MajorID) REFERENCES Major(MajorID) ); -- 教师表 CREATE TABLE Teacher ( TeacherID CHAR(8) PRIMARY KEY, Name NVARCHAR(20) NOT NULL, Title NVARCHAR(20), DeptID INT NOT NULL, CONSTRAINT FK_Teacher_Dept FOREIGN KEY (DeptID) REFERENCES Department(DeptID) ); -- 课程表 CREATE TABLE Course ( CourseID CHAR(8) PRIMARY KEY, CourseName NVARCHAR(50) NOT NULL, Credit DECIMAL(3,1) NOT NULL CHECK (Credit > 0), CourseType NVARCHAR(10) CHECK (CourseType IN ('必修','选修')) ); -- 开课表 CREATE TABLE CourseOffering ( OfferingID INT IDENTITY(1,1) PRIMARY KEY, CourseID CHAR(8) NOT NULL, TeacherID CHAR(8) NOT NULL, Semester NVARCHAR(20) NOT NULL, Capacity INT NOT NULL DEFAULT 60, Enrolled INT NOT NULL DEFAULT 0, CONSTRAINT FK_Offering_Course FOREIGN KEY (CourseID) REFERENCES Course(CourseID), CONSTRAINT FK_Offering_Teacher FOREIGN KEY (TeacherID) REFERENCES Teacher(TeacherID), CONSTRAINT CK_Enrolled CHECK (Enrolled >= 0 AND Enrolled <= Capacity) ); -- 选课表 CREATE TABLE Enrollment ( EnrollmentID INT IDENTITY(1,1) PRIMARY KEY, StudentID CHAR(10) NOT NULL, OfferingID INT NOT NULL, SelectTime DATETIME NOT NULL DEFAULT GETDATE(), Score DECIMAL(5,1) NULL, CONSTRAINT FK_Enroll_Student FOREIGN KEY (StudentID) REFERENCES Student(StudentID), CONSTRAINT FK_Enroll_Offering FOREIGN KEY (OfferingID) REFERENCES CourseOffering(OfferingID), CONSTRAINT UQ_Student_Offering UNIQUE (StudentID, OfferingID) ); GO逻辑说明:先建字典表(院系、专业),再建主表(学生、教师、课程),最后建关联表(开课、选课),这样外键引用不会报错。CK_Enrolled约束保证已选人数不会超过容量也不会为负,这是数据库层面的兜底。
参数说明:CHAR(10)存学号固定 10 位,比VARCHAR省空间;DECIMAL(3,1)存学分,支持 0.5 学分;GETDATE()自动记录选课时间。
3.2 选课存储过程:事务 + 行锁防超选
选课不是简单 INSERT,要同时做三件事:检查容量、插入选课记录、更新已选人数。这三步必须在一个事务里,否则并发时会超选。
CREATE PROCEDURE sp_SelectCourse @StudentID CHAR(10), @OfferingID INT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; -- 加行锁读取开课信息,防止并发超选 DECLARE @Capacity INT, @Enrolled INT; SELECT @Capacity = Capacity, @Enrolled = Enrolled FROM CourseOffering WITH (UPDLOCK, ROWLOCK) WHERE OfferingID = @OfferingID; IF @Enrolled >= @Capacity BEGIN ROLLBACK TRANSACTION; RAISERROR('课程已满', 16, 1); RETURN; END -- 插入选课记录,唯一约束会拦截重复选课 INSERT INTO Enrollment (StudentID, OfferingID) VALUES (@StudentID, @OfferingID); -- 更新已选人数 UPDATE CourseOffering SET Enrolled = Enrolled + 1 WHERE OfferingID = @OfferingID; COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; THROW; END CATCH END GO逻辑说明:WITH (UPDLOCK, ROWLOCK)是关键,它在读取开课记录时就加更新锁,其他会话想同时选这门课会被阻塞,等第一个事务提交后再读,从而避免两个学生同时读到「还剩 1 个名额」然后都插入成功。TRY...CATCH保证出错时回滚,不会留下脏数据。
参数说明:@StudentID和@OfferingID由调用方传入。RAISERROR抛出自定义错误,前端可以捕获提示「课程已满」。
3.3 退课与成绩录入的配套脚本
退课逻辑和选课相反,先删选课记录,再减已选人数,同样要事务。
CREATE PROCEDURE sp_DropCourse @StudentID CHAR(10), @OfferingID INT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; DELETE FROM Enrollment WHERE StudentID = @StudentID AND OfferingID = @OfferingID; IF @@ROWCOUNT = 0 BEGIN ROLLBACK TRANSACTION; RAISERROR('未找到选课记录', 16, 1); RETURN; END UPDATE CourseOffering SET Enrolled = Enrolled - 1 WHERE OfferingID = @OfferingID; COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; THROW; END CATCH END GO成绩录入用一条 UPDATE 即可,但要限制分数范围。
CREATE PROCEDURE sp_InputScore @EnrollmentID INT, @Score DECIMAL(5,1) AS BEGIN IF @Score < 0 OR @Score > 100 BEGIN RAISERROR('分数必须在0-100之间', 16, 1); RETURN; END UPDATE Enrollment SET Score = @Score WHERE EnrollmentID = @EnrollmentID; END GO逻辑说明:退课先删记录再减人数,顺序不能反,否则删失败时人数已经减了。成绩录入先校验范围,避免脏数据进库。
参数说明:@@ROWCOUNT判断删除是否命中记录,没命中说明学生根本没选这门课,直接报错回滚。
4. 查询与统计:答辩最常被问的几张报表怎么写
4.1 学生课表查询与已选学分统计
学生登录后要看自己的课表,这条查询要联三张表:选课表、开课表、课程表。
SELECT c.CourseName, t.Name AS TeacherName, o.Semester, c.Credit, e.Score FROM Enrollment e JOIN CourseOffering o ON e.OfferingID = o.OfferingID JOIN Course c ON o.CourseID = c.CourseID JOIN Teacher t ON o.TeacherID = t.TeacherID WHERE e.StudentID = '2024010001' ORDER BY o.Semester, c.CourseID;已选学分统计要注意:只有成绩及格(>=60)或成绩为空(在修)的课才算学分,挂科的不算。
SELECT SUM(c.Credit) AS TotalCredit FROM Enrollment e JOIN CourseOffering o ON e.OfferingID = o.OfferingID JOIN Course c ON o.CourseID = c.CourseID WHERE e.StudentID = '2024010001' AND (e.Score IS NULL OR e.Score >= 60);逻辑说明:Score IS NULL表示还在修,先计入;Score >= 60表示已通过。两者取或,挂科的自动排除。
4.2 课程选课人数与容量对比报表
教务最关心哪些课快满了、哪些课没人选。
SELECT c.CourseName, t.Name AS TeacherName, o.Semester, o.Enrolled, o.Capacity, CAST(o.Enrolled * 100.0 / o.Capacity AS DECIMAL(5,1)) AS FillRate FROM CourseOffering o JOIN Course c ON o.CourseID = c.CourseID JOIN Teacher t ON o.TeacherID = t.TeacherID ORDER BY FillRate DESC;逻辑说明:FillRate是满座率,乘以 100.0 是为了避免整数除法丢精度。CAST保留一位小数,方便排序和展示。
参数说明:如果想只看满座率超过 80% 的课,加WHERE o.Enrolled * 1.0 / o.Capacity > 0.8。
4.3 成绩分布与挂科率统计
答辩老师常问「你怎么统计挂科率」,这条查询直接给答案。
SELECT c.CourseName, COUNT(*) AS TotalCount, SUM(CASE WHEN e.Score < 60 THEN 1 ELSE 0 END) AS FailCount, CAST(SUM(CASE WHEN e.Score < 60 THEN 1 ELSE 0 END) * 100.0 / COUNT(*) AS DECIMAL(5,1)) AS FailRate FROM Enrollment e JOIN CourseOffering o ON e.OfferingID = o.OfferingID JOIN Course c ON o.CourseID = c.CourseID WHERE e.Score IS NOT NULL GROUP BY c.CourseName;逻辑说明:CASE WHEN做条件计数,只统计有成绩的记录。GROUP BY按课程分组,得到每门课的挂科率。
参数说明:WHERE e.Score IS NOT NULL排除在修课程,避免把没出成绩的算成挂科。
5. 避坑与排查:课程设计里最容易翻车的五个点
5.1 并发选课导致超选,现象是已选人数超过容量
现象:两个学生同时选最后一门课,都提示成功,但Enrolled变成Capacity + 1。
原因:读取容量和更新人数之间没有加锁,两个事务都读到了旧值。
解决:在存储过程里用WITH (UPDLOCK, ROWLOCK)读取开课记录,把读和更新锁在同一个事务里。上面sp_SelectCourse已经这么做了。如果不想用锁,也可以在UPDATE时加WHERE Enrolled < Capacity条件,根据@@ROWCOUNT判断是否成功。
5.2 重复选课报主键冲突,而不是友好提示
现象:学生重复点选课按钮,前端收到「违反唯一约束」的英文报错。
原因:唯一约束UQ_Student_Offering拦截了重复插入,但错误信息不友好。
解决:在存储过程里先查一下是否已选,或者用TRY...CATCH捕获唯一约束错误(错误号 2627),转成中文提示「您已选过这门课」。前端也要做按钮防抖。
5.3 删除学生时外键报错,删不掉
现象:想删一个退学的学生,提示外键冲突。
原因:选课表里有该学生的选课记录,外键阻止删除。
解决:两种方案。一是先删选课记录再删学生,用事务包起来;二是给学生表加IsDeleted状态字段,做软删除,不物理删。课程设计里推荐第二种,更贴近真实系统。
5.4 学分统计把挂科也算进去了
现象:学生挂了一门 4 学分的课,总学分还是显示 4 学分。
原因:统计时没加Score >= 60条件。
解决:统计已修学分时用WHERE Score IS NULL OR Score >= 60,只算在修和通过的。这条在 4.1 的查询里已经体现。
5.5 连接 SQL Server 报 SSL 证书链错误
现象:用 ODBC 或某些驱动连接时,报「证书链是由不受信任的颁发机构颁发的」或「客户端无法建立连接」。
原因:驱动默认启用了加密,但本地 SQL Server 用的是自签名证书,不被信任。
解决:在连接字符串里加TrustServerCertificate=True或Encrypt=False。SSMS 里连接时勾选「信任服务器证书」。这是本地开发环境的常见问题,生产环境应该配正规证书而不是关掉加密。
注意:
Encrypt=False只建议在本地课程设计环境用,真实项目不要关加密。
6. 把设计讲成故事:答辩演示与脚本交付的收尾技巧
课程设计最后要交文档和源码,答辩时要讲清楚设计取舍。我的习惯是准备一个「演示脚本」,按顺序跑几条关键 SQL,让老师看到系统是活的。
第一步,插入测试数据。准备 2 个院系、3 个专业、5 个学生、3 个教师、5 门课程、5 条开课记录。数据不用多,但要覆盖各种情况:有必修有选修、有满员有未满、有成绩有在修。
INSERT INTO Department VALUES (1, N'计算机学院'), (2, N'外国语学院'); INSERT INTO Major VALUES (1, N'软件工程', 1), (2, N'计算机科学', 1), (3, N'英语', 2); INSERT INTO Student VALUES ('2024010001', N'张三', '男', 1, 2024), ('2024010002', N'李四', '女', 1, 2024), ('2024010003', N'王五', '男', 2, 2024); INSERT INTO Teacher VALUES ('T001', N'赵老师', N'教授', 1), ('T002', N'钱老师', N'副教授', 1); INSERT INTO Course VALUES ('C001', N'数据库原理', 4.0, N'必修'), ('C002', N'数据结构', 4.0, N'必修'), ('C003', N'日语入门', 2.0, N'选修'); INSERT INTO CourseOffering (CourseID, TeacherID, Semester, Capacity) VALUES ('C001', 'T001', N'2024秋', 60), ('C002', 'T002', N'2024秋', 50), ('C003', 'T001', N'2024秋', 30);第二步,演示选课。调用sp_SelectCourse给张三选数据库原理,再查选课表确认。
EXEC sp_SelectCourse '2024010001', 1; SELECT * FROM Enrollment WHERE StudentID = '2024010001';第三步,演示冲突。让张三再选一次同一门课,展示唯一约束报错;或者把某门课容量改成 1,让两个学生抢,展示第二个被拦截。
第四步,演示统计。跑 4.2 的满座率报表和 4.3 的挂科率报表,说明设计支持教务分析。
交付物方面,文档里要包含 ER 图、表结构说明、存储过程说明、测试用例。源码就是一个.sql文件,按「建库 → 建表 → 存储过程 → 测试数据」的顺序组织,别人拿到能一次跑通。我一般会在脚本开头写清楚执行顺序和注意事项,比如「先执行建库部分,再执行存储过程,最后插入测试数据」。
一个具体技巧:把存储过程的RAISERROR消息统一成中文,答辩时演示报错更直观。另外,Enrollment表的SelectTime默认值用GETDATE(),演示时能看到真实时间戳,比写死时间更有说服力。
血泪经验是:别等到答辩前一天才跑脚本。我见过太多人本地建好了库,换台机器就报外键顺序错误。把脚本从头到尾在干净实例上跑一遍,是唯一的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取