简介:《数据库系统概论》课程设计报告——学生选课管理信息系统,面向高校数据库课程设计、期末大作业与答辩准备场景,适合需要参考完整设计流程与文档写法的本科生及任课教师。压缩包内仅含1个docx文档,大小约3.96MB,按需求分析、概念结构设计、逻辑结构设计、物理实现、功能调试与应用程序设计组织,目录层级清晰。内容覆盖可行性分析与功能需求、组织结构与业务流/数据流分析、数据字典、CDM与PDM建模,以及SQL Server表结构、完整性约束、视图、索引、存储过程和触发器的创建代码;后段还给出学生信息、课程信息、登录与成绩管理模块的调试与设计示例。已有852人学习,可作为课程设计范文、模板与素材,帮助读者快速搭建报告框架、对照SQL实现并梳理答辩要点。
1. 答辩前一周被退回的数据库课程设计,问题往往不在功能
每年这个节点都有人拿着一份《学生选课管理信息系统》的数据库课程设计报告来找我,封面、目录、页码全齐,E-R 图也画了,翻到第四部分却发现建表脚本里SC表的外键指向一张还没建的course表,脚本一执行就报"引用了无效的对象名"。这类报告翻车的地方从来不是"功能没写完",而是需求分析、概念模型、物理实现三段各写各的,接不上。手上这份课程设计报告模板把需求分析、数据字典、CDM/PDM 图、SQL Server 建表、视图、触发器、存储过程、索引、功能调试和应用程序设计串成了一条线,既能直接当数据库课程设计的底稿,也能当毕设前期练手用的范文素材——但前提是你知道哪几处必须自己动手改。
2. 需求分析写到能验收的颗粒度:角色、数据字典与分层数据流图
需求分析这一章最容易被写成流水账,"方便了教师也方便了学生"这类句子写十行也不产生一行 DDL。合格的标准很朴素:这一章写完之后,后面建表时不用再回头补字段、加实体。所以本章按"角色定权限、字典定字段、数据流定边界"三条线拆。
2.1 三类角色与五个功能模块的拆法
原报告把系统用户划成教务管理员、开课教师、选课学生三类,模块拆成课程管理、学生管理、教师管理、查询管理、数据库管理、选课管理。这个拆法有一处不太顺:把"查询"单独列成一个模块,可查询其实是每个模块里的一个动作,教师查名单、学生查成绩、管理员查统计,都不属于同一个功能边界。我一般会把它拆成"业务模块 + 权限模块"两层,业务模块管数据怎么变,权限模块管谁能变。
真正有价值的是把角色和操作落成一张权限矩阵,这张表写进需求分析之后,后面建视图和数据库角色就有依据了,不用临场拍脑袋。
| 功能点 | 教务管理员 | 开课教师 | 选课学生 |
|---|---|---|---|
| 学生/教师信息初始化 | 增、删、改、查 | 只读本人 | 只读本人 |
| 课程开设申请 | 审批、驳回 | 提交、修改 | 浏览 |
| 选课时段开关 | 设置、锁定 | 无 | 无 |
| 选课/退选 | 强制退选 | 查看名单 | 选、退(时段内) |
| 成绩录入 | 归档、锁定 | 录入、修改 | 只读本人 |
| 数据备份还原 | 执行 | 无 | 无 |
落到 SQL Server 上,这三行就是三个数据库角色:db_admin、db_teacher、db_student,视图和存储过程按角色授权,而不是在应用层用 if-else 判断。原报告第六章的登录模块只做了"账号密码对不对",没做"这个账号能看哪些行",这在答辩时是会被追问的点——同样是查成绩,学生只能看自己的,教师只能看自己所教课程的,这个过滤条件放在视图里比放在代码里更稳。
模块划分还要注意一点:功能模块图里的每个叶子节点,都要能在后面的数据流图里找到对应的加工,找不到就说明这个模块是凭空画出来的。原报告的模块图里有"数据库管理"这一支,如果下面没有备份、还原、日志清理这些具体加工,这一支就是虚的,答辩时容易被问住。
2.2 数据字典的字段类型为什么必须一次定死
数据字典是整份报告里最枯燥、也最不能出错的部分。它是后面建表 DDL 的唯一来源,改一个字段长度,建表语句、视图定义、存储过程参数、前端实体类四处都要跟着改,漏一处就是运行时报错。
原报告给出的学生、课程、选课、教师四张字典表,基本结构是对的,字段类型有几处需要动手调:
| 数据项 | 含义 | 原报告类型 | 建议类型 | 理由 |
|---|---|---|---|---|
| Sno | 学号 | Char(10) | Char(10) | 定长、等值查询多,定长比较快 |
| Spassword | 密码 | Char(6) | Varchar(64) | 6 位定长存不下哈希值,且定长会补空格 |
| Sname | 姓名 | Char(10) | Nvarchar(20) | 中文按字符计,Char 按字节补位容易截断 |
| Sdept | 系名 | Char(20) | Varchar(30) | 变长,避免大量空格占页 |
| 邮箱 | Varchar(30) | Varchar(60) | 30 位对部分邮箱不够 | |
| Grade | 成绩 | Int | Decimal(5,2) | 支持 0.5 分制,且要配 CHECK |
| Ccredit | 学分 | Int(3) | Decimal(3,1) | 存在 0.5 学分课程 |
这里有个血泪经验:Char(6)存密码这一条,几乎每份模板里都有,因为它看起来"够用"。但只要你在报告里写了"安全性要求:账户密码不可被他人使用",字段长度就得给哈希留出空间,Varchar(64)是常见做法。同理,成绩字段用Int看似省事,一旦要做平均分、绩点换算,小数部分直接丢失,Decimal(5,2)更合适。
数据字典还要写清楚"谁写入、谁读取"。学号是管理员初始化的、学生不能改;成绩是教师写入的、学生只读;课程学分是教师申请开课时填的、管理员审批后可调。这三句话写进字典备注栏,后面设计触发器和权限时能省掉很多争论。
2.3 数据流图分层:0 层、1 层、2 层各画什么
原报告画了 0 层、1 层、2 层三张数据流图,方向是对的,但很多同学画到 2 层就乱了,原因是没搞清楚每层该画多少东西。
| 层次 | 加工编号 | 画什么 | 本系统对应 |
|---|---|---|---|
| 0 层 | P0 | 整个系统当一个黑匣子,只画外部实体与进出数据流 | 学生/教师/管理员 ↔ 选课管理系统 |
| 1 层 | P1、P2、P3 | 按角色拆成三个子系统加工 | 学生子系统、教师子系统、管理员子系统 |
| 2 层 | P2.1、P2.2… | 单个子系统内部再拆,一般不超过 7 个加工 | 管理员下的用户管理、时段设置、数据备份 |
分层有三条硬规矩。一是父子平衡:1 层的输入输出数据流,必须和 0 层里对应加工的数据流完全一致,不能凭空多出一条。二是数据流闭合:任何一个加工都要有输入有输出,只进不出(比如"学生信息录入"只画了输入没画写入学生表)是最常见的扣分点。三是数据字典里的每个数据项,都要在某一层的数据流上出现过,否则就是字典里写了、图里没用,两边对不上。
画 2 层的时候,加工编号要带父编号,P2.1、P2.2这种写法的好处是评审时一眼能看出它属于哪个父加工。另外,数据存储(也就是那些双横线)在 1 层图上就可以出现,不必等到 2 层,student、course、sc、teacher四张表在 1 层图上标清楚,后面转逻辑模型时几乎可以直接抄。
3. 从 E-R 图到建表语句:概念模型转逻辑模型的三个判定点
概念设计到逻辑设计这一段,是整份报告里最能看出功力的地方。E-R 图画得再漂亮,转不成带主外键的表结构就是白画。这一段的核心工作量集中在三个判断上:哪些东西该独立成表、多对多联系怎么拆、约束写在哪儿。
3.1 实体与联系的识别:哪些该独立成表
原报告识别出学生、课程、教师三个实体,加上管理员和选课记录,一共五张表。这个划分基本正确,但正文里有一处自相矛盾的地方值得拿出来说:正文写"一名学生可以选择多门课程,而一门课程也可以被多名学生选择,所以是 n:m 的联系",可后面的表格却写成"学生 1:N 课程、课程 1:N 教师"。这两处必须统一,否则逻辑模型没法往下推。
判定方法很简单,问两个问题:一个学生能不能选多门课?能。一门课能不能被多个学生选?能。两个都能,就是 M:N,必须拆中间表。教师和课程同理,一个教师能教多门课、一门课能有多个教师,也是 M:N。而管理员对课程审批,一个管理员能审批多门课、一门课只由一个管理员审批,这才是 1:N,外键放在课程表里就行。
还有一个容易漏的点:先修课。原报告在选课记录的数据字典里放了Cpno(先修课号),但这个字段既不属于"某学生选了某课"这个事实,也会导致同一门课有多个先修课时出现重复行。先修课本质上是课程对课程的自反 M:N 联系,正确做法是独立建一张表。
-- 课程先修关系:课程对课程的自反多对多,必须独立成表 CREATE TABLE course_prev ( cno CHAR(7) NOT NULL, -- 本课程号 pcno CHAR(7) NOT NULL, -- 先修课程号 CONSTRAINT pk_course_prev PRIMARY KEY (cno, pcno), CONSTRAINT fk_cp_cno FOREIGN KEY (cno) REFERENCES course(cno), CONSTRAINT fk_cp_pcno FOREIGN KEY (pcno) REFERENCES course(cno), CONSTRAINT ck_cp_self CHECK (cno <> pcno) -- 不允许自己成为自己的先修课 );这段 DDL 的关键在最后两行。复合主键(cno, pcno)保证同一对先修关系不会重复插入;两个外键都指向course(cno),这是自反联系的标志;CHECK (cno <> pcno)把"课程不能是自己的先修课"这条业务规则下沉到数据库,应用层忘了校验也不会写进脏数据。
3.2 多对多联系拆中间表与外键方向
选课关系的中间表SC,字段是学号、课程号、成绩,主键是(sno, cno),两个外键分别指向学生表和课程表。这里有个细节:主键的顺序会直接影响索引的可用性。(sno, cno)这个顺序,天然支持"按学号查这个学生选了哪些课",但"按课程号查这门课有哪些学生"就得全表扫。教师看名单、统计选课人数走的都是后一条路,所以后面必须给cno单独补一个非聚集索引。
授课关系同理,teachingplan(cno, tno)是复合主键,两个外键分别指向课程和教师。如果一门课只允许一个教师主讲,那就不该用 M:N,直接在course表里放tno外键更省事。原报告在这里是两头都想兼顾,既写了"一门课程可以由多名老师授课",又在课程表里放了Tno,两套并存会导致同一事实存两遍,改一处另一处不同步。选一种,别都留。
中间表还要想清楚"成绩为空"的含义。grade允许 NULL,表示"已选课但未出分",这和"没选这门课"是两回事。如果grade设成NOT NULL DEFAULT 0,统计平均分时就会把没出分的记录算成 0 分,数据直接失真。
3.3 主键、外键与完整性约束的落法
建表阶段真正要决策的是"约束放在数据库还是放在代码"。我的习惯是:能在数据库层表达的业务规则,一律下沉到数据库,应用层只做友好提示。原因是课程设计这类系统通常只有一个应用入口,但答辩演示时经常直接用 Management Studio 改数据,代码层的校验根本拦不住。
| 约束类型 | 本系统落点 | 说明 |
|---|---|---|
| 主键 | student(sno)、course(cno)、teacher(tno)、sc(sno,cno) | 中间表用复合主键,不另设自增 ID |
| 外键 | sc.sno→student、sc.cno→course | 默认 NO ACTION,禁止级联删除 |
| 唯一 | course(cname, term) | 同一学期不允许同名课程 |
| 检查 | grade BETWEEN 0 AND 100、ssex IN ('男','女') | 业务规则下沉 |
| 默认 | sc.grade 默认 NULL | 区分"未出分"与"0 分" |
外键的删除策略要特别说明。学生退学、课程下架,都不应该级联删掉选课记录,历史数据要留着做统计和审计。所以外键一律用默认的NO ACTION,真要删就先删子表再删父表,或者在应用层做"逻辑删除"(加一个status字段)。这一点写进报告的设计说明里,比只贴一段建表代码有说服力得多。
4. SQL Server 物化:建表顺序、视图、触发器、存储过程与索引
物理设计这一段是整份报告里唯一"跑起来就能验证对错"的部分。顺序错了脚本报错,触发器写错了数据不对,索引建错了查询慢——每一条都能在功能调试章节里找到对应的测试记录。
4.1 建表顺序与外键依赖
外键要求被引用的表先存在,所以建表顺序不是随便排的:
student、teacher(无外键依赖)course(依赖teacher.tno)sc(依赖student、course)teachingplan(依赖course、teacher)course_prev(两次依赖course)manage(独立)
原报告的建表脚本是按报告目录顺序贴的,课程表在前、学生表在后,SC表的外键指向还没建的student,直接执行必然失败。规范的写法是脚本开头先按依赖反序删除,再按依赖正序创建,这样脚本可以反复执行不报错。
-- 重建脚本:先按依赖反序删除,再按依赖正序创建 IF OBJECT_ID('dbo.sc', 'U') IS NOT NULL DROP TABLE dbo.sc; IF OBJECT_ID('dbo.course_prev', 'U') IS NOT NULL DROP TABLE dbo.course_prev; IF OBJECT_ID('dbo.teachingplan', 'U') IS NOT NULL DROP TABLE dbo.teachingplan; IF OBJECT_ID('dbo.course', 'U') IS NOT NULL DROP TABLE dbo.course; IF OBJECT_ID('dbo.student', 'U') IS NOT NULL DROP TABLE dbo.student; IF OBJECT_ID('dbo.teacher', 'U') IS NOT NULL DROP TABLE dbo.teacher; GO CREATE TABLE teacher ( tno CHAR(5) NOT NULL, tname NVARCHAR(20) NOT NULL, tdept VARCHAR(30) NOT NULL, duty NVARCHAR(10) NULL, -- 职称 telephone VARCHAR(12) NULL, CONSTRAINT pk_teacher PRIMARY KEY (tno) ); CREATE TABLE course ( cno CHAR(7) NOT NULL, cname NVARCHAR(40) NOT NULL, term INT NOT NULL, -- 开课学期,如 202401 tno CHAR(5) NULL, -- 主讲教师,允许暂缺 ccredit DECIMAL(3,1) NOT NULL, capacity INT NOT NULL DEFAULT 60, -- 选课容量,模板里常缺这个字段 CONSTRAINT pk_course PRIMARY KEY (cno), CONSTRAINT fk_course_teacher FOREIGN KEY (tno) REFERENCES teacher(tno), CONSTRAINT ck_course_credit CHECK (ccredit > 0 AND ccredit <= 10), CONSTRAINT ck_course_capacity CHECK (capacity > 0) ); CREATE TABLE sc ( sno CHAR(10) NOT NULL, cno CHAR(7) NOT NULL, grade DECIMAL(5,2) NULL, -- NULL 表示已选未出分 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_sc_grade CHECK (grade IS NULL OR (grade >= 0 AND grade <= 100)) );course.capacity这个字段原报告没有,但选课系统不给容量上限,触发器里就没有东西可校验,答辩时"选课人数超了怎么办"这个问题没法回答。加字段属于合理补全,报告里加一句设计说明即可。
ck_sc_grade这一条写成grade IS NULL OR (...),而不是直接grade BETWEEN 0 AND 100,是因为NULL BETWEEN 0 AND 100结果是 UNKNOWN,约束会放行(这正好符合预期),但写清楚意图能避免别人误读。
4.2 视图设计:三类用户各看什么
视图在这个系统里承担两件事:一是按角色裁剪列(学生不该看到密码字段),二是按角色裁剪行(学生只能看自己的成绩)。这两件事用视图做比用应用代码做更可靠,因为直接连数据库也绕不过去。
-- 学生成绩视图:只暴露学号、姓名、课程名、学分、成绩,不出现任何密码字段 CREATE VIEW vs_student_score WITH SCHEMABINDING AS SELECT s.sno, s.sname, c.cno, c.cname, c.ccredit, sc.grade FROM dbo.student AS s JOIN dbo.sc AS sc ON sc.sno = s.sno JOIN dbo.course AS c ON c.cno = sc.cno; GO -- 教师名单视图:只给教师看自己所教课程的选课名单 CREATE VIEW vs_teacher_roster AS SELECT c.tno, c.cno, c.cname, s.sno, s.sname, s.sdept, sc.grade FROM course c JOIN sc sc ON sc.cno = c.cno JOIN student s ON s.sno = sc.sno; GOWITH SCHEMABINDING的作用是把视图和基表的列绑定起来,基表想改列类型或删列必须先删视图,避免"视图建好了、基表一改就报错"这种隐蔽故障。代价有两条:查询里必须写两段式名称dbo.xxx,且不能用SELECT *。这两条看着麻烦,其实是好事,逼着你把列清单写死。
vs_teacher_roster里没有加WHERE tno = ...过滤,是因为视图没法知道当前登录的是谁。常见做法是在应用层传参查这个视图,或者给每个教师建一个带CURRENT_USER判断的视图(不推荐,用户一多就是灾难)。报告里写清楚"行级过滤在应用层拼参数完成",比含糊带过要专业。
WITH CHECK OPTION这个选项要慎用。它只对可更新的简单视图有效,一旦视图里带 JOIN 或聚合,更新本来就不允许,加了也是摆设。原报告里给学生基本信息建的视图是单表查询,那种场景才适合加。
4.3 触发器:选课容量与成绩录入边界
触发器要解决的是"跨表的业务规则"。选课这个动作,既要往sc插一行,又要确认这门课没满、这个学生没重复选、先修课已修完——三件事必须原子完成。用触发器做前置校验,比在应用层写三个 if 更稳,因为它对所有写入路径都生效。
CREATE TRIGGER trg_sc_insert_check ON sc AFTER INSERT AS BEGIN SET NOCOUNT ON; -- 1) 重复选课:按 inserted 集合判断,不要用变量接单行 IF EXISTS ( SELECT 1 FROM inserted i JOIN sc ON sc.sno = i.sno AND sc.cno = i.cno GROUP BY i.sno, i.cno HAVING COUNT(*) > 1 ) BEGIN RAISERROR('该学生已选过此课程', 16, 1); ROLLBACK TRANSACTION; RETURN; END -- 2) 容量校验:统计已选人数是否超过课程容量 IF EXISTS ( SELECT 1 FROM inserted i JOIN course c ON c.cno = i.cno WHERE (SELECT COUNT(*) FROM sc WHERE sc.cno = i.cno) > c.capacity ) BEGIN RAISERROR('该课程选课人数已满', 16, 1); ROLLBACK TRANSACTION; RETURN; END -- 3) 先修课校验:存在未通过的先修课则拒绝 IF EXISTS ( SELECT 1 FROM inserted i JOIN course_prev cp ON cp.cno = i.cno WHERE NOT EXISTS ( SELECT 1 FROM sc WHERE sc.sno = i.sno AND sc.cno = cp.pcno AND sc.grade >= 60 ) ) BEGIN RAISERROR('先修课程未通过,不能选修本课程', 16, 1); ROLLBACK TRANSACTION; RETURN; END END;逻辑说明:三个校验全部基于inserted这张逻辑表做集合判断,没有一处写SELECT @sno = sno FROM inserted。后者是触发器里最常见的翻车写法——一次插入多行时,变量只会拿到其中一行,其余行静默通过校验。参数说明:RAISERROR的第二个参数16表示一般性用户错误,能被应用层捕获;ROLLBACK TRANSACTION放在RETURN之前,确保校验失败时整个语句回滚。
成绩触发器只做一件事:成绩一旦录入并被管理员归档,就不允许再改。
CREATE TRIGGER trg_sc_grade_lock ON sc AFTER UPDATE AS BEGIN SET NOCOUNT ON; IF EXISTS ( SELECT 1 FROM inserted i JOIN deleted d ON d.sno = i.sno AND d.cno = i.cno WHERE d.grade IS NOT NULL AND i.grade <> d.grade AND EXISTS (SELECT 1 FROM course WHERE cno = i.cno AND term < 202401) ) BEGIN RAISERROR('往期课程成绩已归档,不允许修改', 16, 1); ROLLBACK TRANSACTION; END END;这里用inserted和deleted对比,才能判断出"成绩被改过"而不是"成绩被写入"。AFTER UPDATE触发器里两个逻辑表同时存在,这是它和 INSERT 触发器的关键差别。
4.4 存储过程与索引:把查询压到索引上
选课这个动作如果只靠触发器,有个绕不开的问题:并发。两个学生同时抢最后一个名额,两边都查到"当前 59 人",都判断"没满",最后插进去 61 人。触发器里的COUNT(*)在默认隔离级别下读不到对方未提交的插入,拦不住这种情况。
CREATE PROCEDURE usp_enroll @sno CHAR(10), @cno CHAR(7) AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; -- 出错自动回滚,不用手写一堆 IF @@ERROR BEGIN TRY BEGIN TRANSACTION; -- 对课程行加更新锁,把并发选课串行化到同一门课上 IF NOT EXISTS ( SELECT 1 FROM course WITH (UPDLOCK, HOLDLOCK) WHERE cno = @cno ) THROW 50001, '课程不存在', 1; IF EXISTS (SELECT 1 FROM sc WHERE sno = @sno AND cno = @cno) THROW 50002, '已选过该课程', 1; INSERT INTO sc (sno, cno, grade) VALUES (@sno, @cno, NULL); COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; THROW; -- 把原始错误抛给调用方,不吞异常 END CATCH END;逻辑说明:WITH (UPDLOCK, HOLDLOCK)是这段代码的核心,它在course表的目标行上加更新锁并持有到事务结束,后来者必须等前一个事务提交才能继续,容量判断因此变得可信。SET XACT_ABORT ON让运行时错误自动回滚整个事务,省掉逐句判断@@ERROR的样板代码。THROW不带参数重新抛出当前错误,保留了原始错误号和消息,排查时能看到是哪一条校验拦下来的。
索引要跟着查询走,不是越多越好:
-- 按课程查名单、统计选课人数:SC 的主键是 (sno, cno),按 cno 查用不上 CREATE NONCLUSTERED INDEX ix_sc_cno ON sc(cno) INCLUDE (sno, grade); -- 按教师查所教课程 CREATE NONCLUSTERED INDEX ix_course_tno ON course(tno) INCLUDE (cname, term); -- 按学期筛课程 CREATE NONCLUSTERED INDEX ix_course_term ON course(term) INCLUDE (cname, ccredit);INCLUDE的作用是把查询要用到但不需要排序的列放到索引叶子页,查询时只读索引不回表。ix_sc_cno把sno和grade带进去之后,"查某门课的学生名单和成绩"这条最热的 SQL 可以完全走索引。反过来,如果给sc.grade单独建索引,选择性太低(成绩就那么几十个取值),优化器根本不会用,白白拖慢插入。
5. 避坑与排查:五个让报告被打回的具体问题
以下五条都是我在帮人看报告时反复见到的,每条按"现象—原因—解决"写,能对上号的直接改。
5.1 建模与约束阶段的三个坑
坑一:先修课字段放错表。现象:把Cpno放进SC表的数据字典后,一门课有两个先修课时,同一学生的选课记录出现两行,主键冲突。 原因:先修课是课程自身的属性,跟"谁选了这门课"无关,放在选课表里就变成每选一次课重复存一遍。 解决:独立建course_prev(cno, pcno)表,SC表只保留sno、cno、grade三个字段。
坑二:建表脚本顺序错,一执行就报错。现象:脚本跑到SC表时报"引用了无效的对象名 'student'",或者重复执行时报"数据库中已存在名为 'sc' 的对象"。 原因:外键要求父表先存在,而脚本按报告章节顺序贴,没有考虑依赖;同时缺少删除语句,脚本不可重复执行。 解决:脚本开头按依赖反序加IF OBJECT_ID(...) IS NOT NULL DROP TABLE ...,创建时按父表到子表排序,中间用GO分批。
坑三:密码字段定长且明文。现象:Spassword CHAR(6)在演示时看着没问题,一旦要求"密码不可被他人使用",长度放不下哈希值,改成明文存储又和安全性要求自相矛盾。 原因:模板沿用了早期教学案例的字段定义,没有考虑存储形态。 解决:改成VARCHAR(64)存哈希,报告里在数据字典备注栏写明"存储密码摘要,不存明文",安全性要求那一段才站得住。
5.2 调试与演示阶段的另外两个坑
坑四:触发器一次插入多行只校验了一行。现象:单条INSERT选课一切正常,用INSERT ... SELECT批量导入历史选课数据时,容量已满的课程照样插进去了。 原因:触发器里写了SELECT @cno = cno FROM inserted,多行插入时变量只保留最后一行,其余行没进校验逻辑。 解决:所有判断改成基于inserted的EXISTS集合判断,禁止用变量从inserted取值。压测时用一条批量插入语句验证,不要只测单行。
坑五:视图用SELECT *,基表加列后报错。现象:给student表加了一个status字段后,原来的学生视图查不出数据,或者报"列名或所提供值的数目与表定义不匹配"。 原因:视图创建时把*展开成了当时的列清单,基表结构一变就对不上;如果视图还带了SCHEMABINDING,改列会直接失败。 解决:视图一律写显式列清单,加SCHEMABINDING时用两段式表名。改基表结构前先DROP VIEW,改完重建。
6. 让报告能跑起来:测试用例表与答辩前的自检清单
报告写得再厚,答辩现场也是让你打开 Management Studio 当场跑。我一般会让学生准备一张测试用例表,每条用例对应一个约束或触发器,跑一条勾一条,被问到"你怎么证明这个约束生效了"时直接翻表。
| 编号 | 测试场景 | 输入 | 预期结果 |
|---|---|---|---|
| T01 | 正常选课 | 学生 S1 选课程 C1 | 插入成功,sc多一行 |
| T02 | 重复选课 | 同一学生再选 C1 | 报"已选过该课程",行数不变 |
| T03 | 容量已满 | 第 61 人选容量 60 的课 | 报"选课人数已满",事务回滚 |
| T04 | 先修课未过 | 先修课成绩 55 分再选后续课 | 报"先修课程未通过" |
| T05 | 成绩越界 | 录入 150 分 | CHECK约束拒绝 |
| T06 | 归档后改分 | 修改往期课程成绩 | 报"已归档,不允许修改" |
| T07 | 并发抢课 | 两个会话同时执行usp_enroll | 一个成功一个失败,总人数不超容量 |
T07 这条最容易被跳过,但它恰恰是答辩时最有说服力的演示:开两个查询窗口,都先BEGIN TRAN不提交,你会看到第二个窗口卡住等锁,提交第一个之后第二个才继续判断容量。这个"卡住"的现象本身就是加锁生效的证据。
答辩前的自检清单我一般就照这几条走一遍:建表脚本能不能从空库一次跑通、跑第二遍会不会报错;所有视图是不是都写了显式列清单、SCHEMABINDING的视图是不是都用了dbo.前缀;触发器里有没有出现SELECT @变量 = 列 FROM inserted;sc表上按cno查的执行计划是不是走了ix_sc_cno;grade字段里有没有 150 分、-1 分这种脏数据;三个数据库角色的权限是不是真的只到视图和存储过程,没有直接给表权限。
那次一个同学的报告被打回,就是因为自检清单里"脚本跑第二遍"这一条没做——他演示到一半手滑重跑了一次建表脚本,sc表已存在直接报错,后面十分钟全在解释。从那以后我每次交任何带 DDL 的东西,都强制先把"删了重建、建完再删"走一遍,确认脚本真能反复跑,才敢按下提交。希望帮到你。
本文还有配套的精品资源,点击获取