资讯详情 SQL Server学生选课系统数据库设计:从建表到存储过程的完整T-SQL源码
📅 2026/10/13 4:38:09
简介面向数据库课程设计与学生选课业务场景这份基于SQL Server的源码与文档包提供了完整可运行的选课系统数据库方案涵盖建表、关系设计、示例数据与常用查询逻辑适合软件工程、计算机科学等专业学生完成期末大作业或课程设计也可作为SQL Server入门练习参考。压缩包共6个文件核心为一个SQL脚本、一份docx详细设计文档、一份md说明以及多张png截图包体仅约139KB轻量易用便于直接下载、修改与复现。资源已在Windows 10/11及mac环境测试运行通过已有441人学习浏览。通过其中的设计文档与SQL源码读者可掌握学生、课程、选课关系等典型表结构设计思路对照作者高分通过的答辩项目进行二次开发或功能扩展快速构建属于自己的数据库课程设计方案。1. SQL Server学生选课系统数据库设计一份能直接交的课程设计源码长什么样做数据库原理课设最怕的不是不会写SQL而是建完表才发现缺外键、缺约束存储过程跑起来全是报错答辩时被老师一句「两个学生同时选同一门只剩一个名额的课会怎样」直接问住。这套基于SQL Server的学生选课系统数据库设计内容就是选课场景下从建库、建表、加约束到视图、存储过程、触发器、测试数据的一整套T-SQL源码外加一份写好的课程设计文档。解压后是sql.sql全量脚本、document.docx设计说明、README.md运行步骤和几张ER图截图结构很干脆。它压中了「课程设计、大作业、期末验收」三个场景拿它改个库名和字段就能交。适合计科、软件工程、大数据这些专业打算用SQL Server做数据库课设又不想从零排坑的同学。2. 先立骨架再写SQLE-R模型、建库规范与关键选型2.1 先画E-R图再写SQL选课业务的对象拆法学生选课系统听起来简单但绝大多数翻车案例都是因为跳过了建模这一步直接打开SSMS建表。我一般会先把业务拆成实体和关系学生属于班级教师开设课程学生选课产生成绩。这里最核心的建模决策是「学生和课程之间的多对多关系必须拆成中间表」。一张选课记录表至少要有学生、课程开设记录、选课时间、成绩、状态这几个字段。为什么不能把课程直接挂在学生表上因为一名学生选多门课一门课被多名学生选这种关系在单表里无法不产生冗余。拆出选课记录表之后学生表存学生信息课程表存课程信息选课表只存「谁在什么时候选了哪门课」三个表各司其职这是答辩时老师第一个会追问的建模问题。班级和学生的关系是一对多放在学生表里用ClassID外键引用。教师和课程的关系则通过开课表落地——同一门课在不同学期可能由不同教师上直接在课程表上加教师字段会丢失历史开课信息。所以整体拆下来是学生表、班级表、教师表、课程表、开课表、选课记录表六张基础表再加一张审计日志表。这套结构对应sql.sql里实际建好的对象你打开脚本能看到同样的表名。2.2 CREATE DATABASE 模板兼容级别、排序规则与文件初始化建库脚本是整套资源里最容易被忽略、却也最影响后续体验的一段。常见做法是把排序规则、初始文件大小和兼容级别一次性定好避免后面改库结构。下面这段是标准的课程设计建库模板-- 建库FILENAME 路径按本机 SQL Server 数据目录调整 CREATE DATABASE StudentCourseDB ON PRIMARY ( NAME NStudentCourseDB, FILENAME ND:\SQLData\StudentCourseDB.mdf, SIZE 16MB, MAXSIZE 1024MB, FILEGROWTH 16MB ) LOG ON ( NAME NStudentCourseDB_log, FILENAME ND:\SQLData\StudentCourseDB_log.ldf, SIZE 8MB, MAXSIZE 512MB, FILEGROWTH 8MB ) COLLATE Chinese_PRC_CI_AS; GO -- 兼容级别150 对应 SQL Server 2019老师机器版本低就调 130 或 120 ALTER DATABASE StudentCourseDB SET COMPATIBILITY_LEVEL 150; GOFILENAME指定了数据文件和日志文件的物理路径我建议放在非系统盘不然默认落到C盘后面日志一膨胀C盘直接红。SIZE和FILEGROWTH控制初始大小和自动增长步长课设数据量下16MB起步够用。COLLATE Chinese_PRC_CI_AS是简体中文、不区分大小写的排序规则指定它之后中文排序和比较行为才符合预期。关键参数是COMPATIBILITY_LEVEL。如果老师本机是SQL Server 2012兼容级别110或2016130你把库建在2019上再交过去低版本打不开或语法报错。我一般按老师环境最保守的版本设置或者交之前确认一下对方实例版本再调。2.3 数据类型与命名规范让表结构一眼看懂数据类型的选型直接决定约束能不能写对。下面是这套资源里用到的推荐组合也是绝大多数课程设计评分表里会看的点字段场景推荐类型不推荐理由学号、工号CHAR(10)VARCHAR(20)定长字符串避免前导0丢失且索引效率更高自增主键INT IDENTITY(1,1)BIGINT课设数据量INT够用BIGINT白占空间学分NUMERIC(4,1)FLOATNUMERIC精确FLOAT有浮点误差成绩NUMERIC(5,1)INT保留一位小数加权统计时不丢精度中文姓名NVARCHAR(20)VARCHAR(20)NVARCHAR按字符存储避免中文乱码命名规范方面主键用PK_前缀、外键FK_、唯一约束UX_、普通索引IX_表名单数。这样做不是为了好看而是老师打开sys.indexes或看ER图时能直接读出关系。sql.sql里建的所有约束都是这个前缀风格你在此基础上加表也不容易乱。3. 三张核心表的T-SQL实现从建表到约束的完整落法3.1 学生与班级表主键、唯一约束与默认值学生和班级是最底层的两张基础表先建班级再建学生因为学生表要引用班级表的主键。sql.sql里的学生表脚本和下面这段核心结构一致我拆开解释每个约束的作用CREATE TABLE dbo.Class ( ClassID INT IDENTITY(1,1) NOT NULL, ClassName NVARCHAR(50) NOT NULL, MajorName NVARCHAR(50) NOT NULL, EnrollmentYear SMALLINT NOT NULL, CONSTRAINT PK_Class PRIMARY KEY CLUSTERED (ClassID), CONSTRAINT UX_Class_ClassName UNIQUE (ClassName) ); GO CREATE TABLE dbo.Student ( StudentNo CHAR(10) NOT NULL, -- 学号业务主键 StudentName NVARCHAR(20) NOT NULL, Gender NCHAR(1) NOT NULL, BirthDate DATE NULL, ClassID INT NOT NULL, EnrollDate DATE NOT NULL DEFAULT GETDATE(), Status TINYINT NOT NULL DEFAULT 1, -- 1 在读0 离校 CONSTRAINT PK_Student PRIMARY KEY CLUSTERED (StudentNo), CONSTRAINT FK_Student_Class FOREIGN KEY (ClassID) REFERENCES dbo.Class (ClassID) ON DELETE NO ACTION, CONSTRAINT CK_Student_Gender CHECK (Gender IN (N男, N女)), CONSTRAINT CK_Student_Status CHECK (Status IN (0, 1)) ); GO学号选CHAR(10)做主键是业务习惯——学号在校园场景里是自然键直接当主键省去关联查询时的一次JOIN。但代价也有如果学校允许转专业改学号改主键会连锁更新所有子表。课程设计层面接受这个取舍文档里把这个理由写进去就是加分项。Gender和Status两个CHECK约束把脏数据挡在数据库层而不是靠应用层判断。EnrollDate用DEFAULT GETDATE()插入时不用显式传值。班级表上的UX_Class_ClassName唯一约束保证不会出现两个同名的班级这是很多随手建表的同学会漏掉的地方。3.2 课程、教师与开课表一门课为什么不能直接挂在教师名下课程表和教师表本身不复杂关键在开课表。很多课程设计把教师ID直接加在课程表上看起来省了一张表实际上丢失了「同一课程在不同学期由不同教师授课」这一事实。这套资源把开课独立成CourseOffering容量字段也放在这一层因为限选人数是针对某一次开课而言的CREATE TABLE dbo.Course ( CourseID NCHAR(6) NOT NULL, -- 如 CS1101按院系前缀编码 CourseName NVARCHAR(50) NOT NULL, Credit NUMERIC(4,1) NOT NULL, Hours SMALLINT NOT NULL, CONSTRAINT PK_Course PRIMARY KEY (CourseID), CONSTRAINT CK_Course_Credit CHECK (Credit 0) ); GO CREATE TABLE dbo.Teacher ( TeacherNo CHAR(8) NOT NULL, TeacherName NVARCHAR(20) NOT NULL, Title NVARCHAR(20) NULL, -- 职称允许为空 CONSTRAINT PK_Teacher PRIMARY KEY (TeacherNo) ); GO CREATE TABLE dbo.CourseOffering ( OfferingID INT IDENTITY(1,1) NOT NULL, CourseID NCHAR(6) NOT NULL, TeacherNo CHAR(8) NOT NULL, Semester NCHAR(5) NOT NULL, -- 如 2025-1 Capacity INT NOT NULL DEFAULT 60, -- 限选人数 CONSTRAINT PK_CourseOffering PRIMARY KEY (OfferingID), CONSTRAINT UX_Offering UNIQUE (CourseID, TeacherNo, Semester), CONSTRAINT FK_Offering_Course FOREIGN KEY (CourseID) REFERENCES dbo.Course (CourseID), CONSTRAINT FK_Offering_Teacher FOREIGN KEY (TeacherNo) REFERENCES dbo.Teacher (TeacherNo), CONSTRAINT CK_Offering_Capacity CHECK (Capacity 0) ); GOUX_Offering唯一约束是这段脚本里最值得在文档里写一句的设计它保证同一个学期、同一门课、同一位教师只能有一条开课记录从根上杜绝重复开课。Capacity默认60且必须大于0这是后面存储过程做容量校验的数据基础。CourseID用NCHAR(6)而不是自增INT是因为课程编号有业务含义——前两位代表学院或课程类别后面是序号。这种编码在选课系统里是常态也方便老师看数据时直接识别课程归属。3.3 选课记录表复合主键与两个外键的正确姿势选课记录表是整套设计里最容易写错的一张表。常见错误是用自增ID做主键然后忘了加唯一约束结果同一个学生对同一门课可以反复插入多条记录。正确做法是复合主键CREATE TABLE dbo.Enrollment ( StudentNo CHAR(10) NOT NULL, OfferingID INT NOT NULL, EnrollTime DATETIME NOT NULL DEFAULT GETDATE(), Score NUMERIC(5,1) NULL, -- 未考试为空不用0表示 Status TINYINT NOT NULL DEFAULT 1, -- 1 已选2 退选3 结课 CONSTRAINT PK_Enrollment PRIMARY KEY (StudentNo, OfferingID), CONSTRAINT FK_Enrollment_Student FOREIGN KEY (StudentNo) REFERENCES dbo.Student (StudentNo) ON DELETE CASCADE, CONSTRAINT FK_Enrollment_Offering FOREIGN KEY (OfferingID) REFERENCES dbo.CourseOffering (OfferingID) ON DELETE NO ACTION, CONSTRAINT CK_Enrollment_Score CHECK (Score IS NULL OR (Score 0 AND Score 100)) ); GO复合主键(StudentNo, OfferingID)从数据库层面拦住了重复选课比在应用层先查再插可靠得多。两个外键的级联策略是刻意区分开的删除学生时这个学生的选课记录失去意义ON DELETE CASCADE合理删除开课记录时如果还有历史选课数据应当阻止删除逼着操作者先处理数据所以是NO ACTION。这个「一个级联、一个阻止」的对比是答辩高频题值得写进设计说明。Score字段用NULL表示未考试而不是填0。0分和未考在统计平均分时性质完全不同用0占位会把加权平均硬生生拉低。CHECK约束同时允许NULL和0到100的范围既保证录入数据合法又不妨碍未考状态。复合主键本身是一个聚集索引天然支持按学生查选课记录如果想要按开课记录反向查选课人数再补一个IX_Enrollment_OfferingID非聚集索引即可。4. 让数据库自己干活视图、存储过程与触发器的代码级拆解4.1 视图把多表JOIN封装成答辩现场秒出的成绩单视图的价值在答辩时体现得最明显老师随口问「看一下这个学生的成绩单」你现场写一条四表JOIN容易手抖而提前封装好的视图一条SELECT就出来。sql.sql里的成绩单视图核心逻辑如下CREATE VIEW v_StudentScore AS SELECT s.StudentNo, s.StudentName, c.ClassName, co.Semester, cu.CourseName, e.Score, e.Status FROM dbo.Enrollment e INNER JOIN dbo.Student s ON e.StudentNo s.StudentNo INNER JOIN dbo.CourseOffering co ON e.OfferingID co.OfferingID INNER JOIN dbo.Course cu ON co.CourseID cu.CourseID LEFT JOIN dbo.Class c ON s.ClassID c.ClassID; GO视图把学生、班级、开课、课程四张表的关系一次性串好。这里用INNER JOIN连接选课记录是刻意的——视图只展示已存在的选课关系没有选课记录的学生不会出现在成绩单里。LEFT JOIN接到班级表是为了处理学生可能在班级表中找不到对应的极端情况。我一般会在这个视图基础上再加一个不及格统计视图供老师查看挂科率。用GROUP BY按课程统计比直接在成绩单视图上筛选更直观CREATE VIEW v_CourseFailStats AS SELECT cu.CourseName, co.Semester, COUNT(CASE WHEN e.Score 60 THEN 1 END) AS FailCount, COUNT(*) AS TotalCount FROM dbo.Enrollment e INNER JOIN dbo.CourseOffering co ON e.OfferingID co.OfferingID INNER JOIN dbo.Course cu ON co.CourseID cu.CourseID GROUP BY cu.CourseName, co.Semester; GO注意COUNT(CASE WHEN ...)的写法它只统计不及格的行而不会把NULL成绩算进去。如果你用SUM(Score60)这种写法遇到NULL会直接报错或产生错误结果。视图建好后答辩时跑这两条SELECT比现场敲JOIN体面得多。4.2 存储过程选课登记中用事务和UPDLOCK堵死并发超选存储过程是这套设计的重头戏。选课登记这个操作看起来简单实际上藏着并发问题一门课只剩一个名额两个学生同时提交如果只是先SELECT再INSERT两个事务都能通过容量检查最后超选。解决办法是给容量行加UPDLOCK锁CREATE PROCEDURE usp_EnrollCourse StudentNo CHAR(10), OfferingID INT AS BEGIN SET NOCOUNT ON; BEGIN TRAN; -- 第一步检查是否已选过这门课 IF EXISTS (SELECT 1 FROM dbo.Enrollment WHERE StudentNo StudentNo AND OfferingID OfferingID) BEGIN ROLLBACK; RAISERROR(N该学生已选此课程, 16, 1); RETURN; END -- 第二步锁定容量行防止两个事务同时通过容量检查 DECLARE capacity INT; SELECT capacity Capacity FROM dbo.CourseOffering WITH (UPDLOCK, ROWLOCK) WHERE OfferingID OfferingID; IF capacity IS NULL BEGIN ROLLBACK; RAISERROR(N开课记录不存在, 16, 1); RETURN; END -- 第三步事务内统计已选人数与容量比较 IF (SELECT COUNT(*) FROM dbo.Enrollment WHERE OfferingID OfferingID) capacity BEGIN ROLLBACK; RAISERROR(N课程容量已满, 16, 1); RETURN; END -- 第四步插入选课记录并提交 INSERT INTO dbo.Enrollment (StudentNo, OfferingID) VALUES (StudentNo, OfferingID); COMMIT; END; GOSET NOCOUNT ON是为了减少无谓的「受影响行数」消息让存储过程的消息输出更干净。BEGIN TRAN到COMMIT/ROLLBACK之间是一整条事务边界任一步出错整个选课操作全部回滚不会出现「容量检查过了但插入失败」的中间状态。WITH (UPDLOCK, ROWLOCK)是这段脚本的精髓。UPDLOCK让两个并发事务在读取同一行容量时互相排他第二个事务必须等第一个提交后才能读此时容量已经变化超选被堵死。ROWLOCK把锁粒度限制在这一行避免锁住整张CourseOffering表拖慢其他操作。课程设计里写出这一步已经是拉开档次的设计了。对应的成绩录入存储过程简单很多核心是UPDATE加行数校验CREATE PROCEDURE usp_SubmitScore StudentNo CHAR(10), OfferingID INT, Score NUMERIC(5,1) AS BEGIN SET NOCOUNT ON; UPDATE dbo.Enrollment SET Score Score, Status 3 WHERE StudentNo StudentNo AND OfferingID OfferingID; IF ROWCOUNT 0 RAISERROR(N未找到选课记录, 16, 1); END; GOROWCOUNT返回上次UPDATE影响的行数如果为0说明传入的学号或开课ID在选课表中不存在直接报错。这个模式比先SELECT判断再UPDATE少一次查询也避免了两条语句之间数据被改掉的时间窗口。4.3 触发器成绩修改留痕答辩讲「审计」的资本触发器不是课设必选项但加了它答辩时就能讲出「数据审计」这个亮点。需求很简单任何人对成绩字段的修改都要记录下来包括旧值、新值、操作人和时间CREATE TABLE dbo.ScoreAudit ( AuditID INT IDENTITY(1,1) PRIMARY KEY, StudentNo CHAR(10) NOT NULL, OfferingID INT NOT NULL, OldScore NUMERIC(5,1) NULL, NewScore NUMERIC(5,1) NULL, ChangedBy NVARCHAR(50) NOT NULL DEFAULT SUSER_SNAME(), ChangedTime DATETIME NOT NULL DEFAULT GETDATE() ); GO CREATE TRIGGER trg_ScoreAudit ON dbo.Enrollment AFTER UPDATE AS BEGIN SET NOCOUNT ON; IF UPDATE(Score) BEGIN INSERT INTO dbo.ScoreAudit (StudentNo, OfferingID, OldScore, NewScore) SELECT i.StudentNo, i.OfferingID, d.Score, i.Score FROM inserted i INNER JOIN deleted d ON i.StudentNo d.StudentNo AND i.OfferingID d.OfferingID WHERE ISNULL(d.Score, -1) ISNULL(i.Score, -1); END END; GOSQL Server触发器里inserted和deleted两张虚拟表分别保存更新后的行和更新前的行。UPDATE(Score)函数判断这次更新是否涉及Score列避免修改Status字段时也触发审计。ISNULL(d.Score, -1) ISNULL(i.Score, -1)这行比较的是新旧值是否真的变化-1是一个不可能出现在合法成绩里的哨兵值这样NULL到NULL的更新不会被误记。SUSER_SNAME()取当前登录名不用额外传参就能记录操作人。这个触发器建好之后每次成绩变更都会自动落一条审计记录答辩时展示ScoreAudit表就是实打实的「数据库安全性设计」。4.4 造数脚本把演示数据撑到答辩不慌空表演示没有任何说服力老师想看的是一千个学生、几百条选课记录下的查询表现。sql.sql里带了基础测试数据我习惯在此基础上再补一个随机选课脚本模拟批量操作SET STATISTICS TIME ON; DECLARE i INT 1; WHILE i 100 BEGIN INSERT INTO dbo.Enrollment (StudentNo, OfferingID) SELECT TOP 1 s.StudentNo, o.OfferingID FROM dbo.Student s CROSS JOIN dbo.CourseOffering o WHERE NOT EXISTS ( SELECT 1 FROM dbo.Enrollment e WHERE e.StudentNo s.StudentNo AND e.OfferingID o.OfferingID ) ORDER BY NEWID(); IF ROWCOUNT 0 BREAK; -- 所有组合都选完了跳出循环 SET i i 1; END; SET STATISTICS TIME OFF; GO这段脚本用CROSS JOIN生成学生与开课记录的全组合NOT EXISTS过滤掉已存在的选课组合ORDER BY NEWID()随机挑一组插入。NEWID()每次调用都生成新值所以排序结果随机模拟出的数据分布接近真实。SET STATISTICS TIME ON会在消息窗口显示每条语句的CPU耗时和总耗时答辩时跑一发指给老师看执行时间比嘴上说「我的设计快」有说服力得多。循环里每次只插一条虽然慢但模拟的是逐个提交选课请求的真实场景。如果只想快速灌数据把INSERT改成批量SELECT一次插几百条也行但那样演示不出存储过程的并发控制效果。5. 课设避坑指南五个把C盘和心态一起搞崩的SQL Server问题5.1 C盘爆红事务日志无限膨胀现象跑了几次存储过程和批量更新之后发现C盘空间急剧减少数据库相关的.ldf文件涨到几个GBSQL Server服务直接罢工。原因新创建的数据库默认是FULL恢复模式事务日志不会自动截断。课设环境没有人配置定期备份作业日志文件只增不减直到写满磁盘。解决课设阶段换成SIMPLE恢复模式然后把日志文件收缩回合理大小ALTER DATABASE StudentCourseDB SET RECOVERY SIMPLE; GO DBCC SHRINKFILE (NStudentCourseDB_log, 64); GOSIMPLE模式下SQL Server会在每次检查点时自动截断不活跃的日志日志文件不会再持续膨胀。DBCC SHRINKFILE的第二个参数是目标大小单位是MB这里收缩到64MB。生产环境千万别这么干但课程设计环境追求的是稳定复现SIMPLE模式是最省心的选择。5.2 删除课程被外键卡死现象想清空Course表执行DELETE FROM dbo.Course;直接报错提示外键约束冲突。原因选课记录表和开课表的外键都引用课程表而这两个外键故意设计成NO ACTION数据库按设计阻止删除。解决按「先删子表、再删父表」的顺序清理DELETE FROM dbo.Enrollment; DELETE FROM dbo.CourseOffering; DELETE FROM dbo.Course; GO如果不想清数据只是要删一门课应该先确认这本文还有配套的精品资源点击获取