简介:本资源是西北工业大学《数据库原理》课程的第五份实验报告,面向计算机专业本科生及数据库初学者,聚焦数据库高级对象的实践操作与原理理解。报告完整覆盖数据表管理、加密与参数化存储过程(jsearch/jmsearch)的创建与调试、多类型触发器(INSERT/DELETE/UPDATE)的设计与验证,包括外键约束处理、级联删除、字段更新拦截及成绩统计自动同步等典型场景,内容紧贴教学大纲且具备工程参考价值。资源为单文件Word文档(.doc),大小231KB,结构清晰含实验步骤、SQL代码、执行结果与分析说明,便于直接复现与课后复习。目前已有716人学习下载,适合需要巩固T-SQL编程能力、掌握数据库安全机制与业务逻辑封装方法的学习者系统演练与查漏补缺。
1. 西北工业大学数据库实验报告5:一份能跑通的 SQL Server 触发器与存储过程实战手记
这不是一份“交完就扔”的实验报告,而是一套在 SQL Server 2016/2019 环境下真实可复现、可调试、可扩展的数据库核心功能训练集。它覆盖了企业级开发中高频使用的三大硬核能力:带参数的存储过程(jsearch)、加密型存储过程(jmsearch)、多类型触发器(INSERT/DELETE/UPDATE/AFTER/INSTEAD OF)——全部基于经典 SPJ 和 Student 教学数据库模型,但每一段代码都经得起生产环境推演。比如insert_s触发器用INSTEAD OF拦截非法插入,dele_s2用AFTER DELETE级联清理子表,update_sc_cavggrade实现成绩表变更自动同步统计表——这些不是概念演示,而是你明天写 ERP 订单状态变更逻辑、HR 员工离职级联归档、教务系统成绩更新实时统计时,真正要抄的骨架。适合刚学完 T-SQL 基础、正卡在“语法会写,一跑就错”阶段的本科生,也适合需要快速捡起 SQL Server 传统运维能力的转岗工程师。别被“实验报告”四个字骗了——这份文档里藏着的,是 SQL Server 开发中最容易翻车、又最常被面试官深挖的 7 类典型场景。
2. 存储过程实战:从参数化查询到加密保护,一步到位
2.1 jsearch:带输入参数的存储过程,解决“按工程号查供应商+零件+工程名”需求
这是典型的多表关联查询封装。原始 SQL 直接写在应用层易出错、难复用、权限松散;封装成存储过程后,业务逻辑收口、执行计划可重用、调用方只需传参。关键点在于:
- 参数类型必须与
JNO字段一致(nchar(20)),否则隐式转换导致索引失效; FROM s,p,j,spj是老式逗号连接,虽能跑通,但强烈建议改用显式JOIN提升可读性与优化器识别率;WHERE条件顺序影响执行效率,应把高选择性字段(如spj.jno = search_jno)放在最前。
-- 推荐写法(显式 JOIN + WHERE 优化) CREATE PROCEDURE jsearch @search_jno nchar(20) AS BEGIN SET NOCOUNT ON; -- 避免返回影响行数消息,减少网络开销 SELECT j.jname AS 工程名称, s.sname AS 供应商名称, p.pname AS 零件名称 FROM spj INNER JOIN j ON spj.jno = j.jno INNER JOIN s ON spj.sno = s.sno INNER JOIN p ON spj.pno = p.pno WHERE spj.jno = @search_jno; END提示:
SET NOCOUNT ON是血泪经验——不加它,每次执行都会返回(X 行受影响)消息,某些 ORM(如早期 Entity Framework)会误判为结果集,导致ExecuteScalar()报错。
调用方式不变:
EXEC jsearch @search_jno = 'J1';2.2 jmsearch:加密存储过程,保护敏感逻辑不被反向工程
WITH ENCRYPTION并非绝对安全(SQL Server 2016+ 可被高级工具解密),但它能有效阻止sp_helptext查看源码,防止初级逆向。注意:加密后无法修改,必须先DROP再重建。原报告中WHERE city = ''是明显笔误(空字符串条件永远为假),实际应为WHERE city IS NOT NULL或指定城市,此处按教学意图修正为通用查询:
-- 正确创建加密存储过程 CREATE PROCEDURE jmsearch WITH ENCRYPTION AS BEGIN SET NOCOUNT ON; SELECT * FROM S; -- 返回所有供应商信息 END验证是否加密:
-- 执行成功,返回结果 EXEC jmsearch; -- 执行失败,报错:“对象 'jmsearch' 的文本已加密” EXEC sp_helptext 'jmsearch';2.3 sp_rename 与 sp_helptext:系统存储过程的边界与陷阱
sp_rename不仅改名,还更新sys.objects元数据,但不会自动更新依赖它的视图、存储过程中的硬编码引用。例如V_SPJ被重命名为V_SPJ_三建后,若某存储过程里写了SELECT * FROM V_SPJ,该过程将直接报错“对象名 'V_SPJ' 无效”。必须手动检查并修改所有调用处。
sp_helptext是调试利器,但仅对未加密对象有效。它输出的是sys.sql_modules.definition字段内容,换行符和缩进会被保留,所以复制粘贴时需注意空格——曾有同学因粘贴时多了一个不可见空格,导致CREATE PROCEDURE语法错误。
3. 触发器深度拆解:INSTEAD OF vs AFTER,一条语句决定数据命运
3.1 insert_s:INSTEAD OF INSERT 触发器,拦截非法插入并自定义反馈
此触发器用于强校验外键约束缺失场景(如C表未建主键或未设外键)。INSTEAD OF的本质是替代原操作,因此必须显式INSERT或ROLLBACK,否则数据不会入库。原报告中IF (EXISTS(...))逻辑正确,但PRINT语句在应用程序中不可见,应改用RAISERROR抛异常,确保调用方能捕获:
CREATE TRIGGER insert_s ON SC INSTEAD OF INSERT AS BEGIN SET NOCOUNT ON; IF EXISTS ( SELECT 1 FROM inserted i LEFT JOIN C ON i.cno = C.cno WHERE C.cno IS NULL ) BEGIN RAISERROR('不能插入这样的记录:课程号 %s 不存在于 C 表中', 16, 1, (SELECT TOP 1 cno FROM inserted)); RETURN; END -- 合法则执行原插入 INSERT INTO SC (sno, cno, grade) SELECT sno, cno, grade FROM inserted; END注意:
RAISERROR第二个参数16表示用户错误级别(11-16 可由用户引发),1是状态码,便于日志分类。
3.2 dele_s1 与 dele_s2:禁止删除 vs 级联删除,两种 DELETE 语义的实现
dele_s1是典型的权限控制型触发器,用INSTEAD OF DELETE直接ROLLBACK并PRINT提示。但生产环境更推荐RAISERROR+XACT_ABORT ON组合,避免事务残留:
-- 生产级写法 CREATE TRIGGER dele_s1 ON S INSTEAD OF DELETE AS BEGIN SET XACT_ABORT ON; -- 遇错立即回滚整个事务 RAISERROR('禁止删除供应商记录:请通过业务流程申请', 16, 1); ENDdele_s2是级联删除,用AFTER DELETE在父表删除成功后清理子表。关键点:deleted临时表只存被删行,IN子查询比JOIN更安全(避免NULL导致全表删除):
CREATE TRIGGER dele_s2 ON S AFTER DELETE AS BEGIN SET NOCOUNT ON; DELETE FROM SC WHERE sno IN (SELECT sno FROM deleted); END3.3 update_s:禁止字段更新的 UPDATE 触发器,精准锁定修改范围
UPDATE()函数是核心——它检测本次 UPDATE 语句是否包含该列,而非值是否变化。原报告IF UPDATE(sdept)正确,但RAISERROR级别应为16(用户错误),且需RETURN阻止后续执行:
CREATE TRIGGER update_s ON S INSTEAD OF UPDATE AS BEGIN SET NOCOUNT ON; IF UPDATE(sdept) BEGIN RAISERROR('sdept 字段禁止修改:如需调整,请提交工单审批', 16, 1); RETURN; END -- 其他字段允许更新 UPDATE S SET sname = i.sname, sage = i.sage, ssex = i.ssex, city = i.city FROM S INNER JOIN inserted i ON S.sno = i.sno; END提示:
INSTEAD OF UPDATE必须手动编写完整UPDATE语句,否则所有字段都会被置为NULL——这是新手最常翻车的黑匣子。
4. 高阶触发器实战:CAvgGrade 统计表自动同步,解决“成绩变更实时统计”难题
4.1 CAvgGrade 表设计与触发器逻辑闭环
原报告中CAvgGrade(o, Snum, examSNum, avgGrade)设计合理:o(课号)为主键,Snum(选课人数)、examSNum(参考人数)、avgGrade(平均分)为统计字段。但触发器存在严重缺陷:
SELECT o FROM inserted无INTO变量,语法错误;COUNT(*)和AVG()在空集时返回NULL,未做ISNULL处理;- 未处理
INSERT和DELETE同时发生(如UPDATE导致inserted和deleted均非空); cgrade >= 0错误——NULL不参与比较,但0是有效成绩,应改为cgrade IS NOT NULL。
修正后的健壮版本:
-- 创建统计表(含默认值防 NULL) CREATE TABLE CAvgGrade ( cno char(10) PRIMARY KEY, Snum int DEFAULT 0, examSNum int DEFAULT 0, avgGrade decimal(5,2) DEFAULT 0.00 ); -- 完整触发器(支持 INSERT/UPDATE/DELETE) CREATE TRIGGER update_sc_cavggrade ON SC FOR INSERT, UPDATE, DELETE AS BEGIN SET NOCOUNT ON; -- 合并 inserted 和 deleted,生成课号集合 DECLARE @cno_list TABLE (cno char(10)); INSERT INTO @cno_list SELECT DISTINCT cno FROM inserted UNION SELECT DISTINCT cno FROM deleted; -- 对每个课号重新计算 DECLARE @cno char(10); DECLARE cur CURSOR FOR SELECT cno FROM @cno_list; OPEN cur; FETCH NEXT FROM cur INTO @cno; WHILE @@FETCH_STATUS = 0 BEGIN DECLARE @Snum int = 0; DECLARE @examSNum int = 0; DECLARE @avgGrade decimal(5,2) = 0.00; -- 选课人数 SELECT @Snum = COUNT(*) FROM SC WHERE cno = @cno; -- 参考人数(grade 非 NULL) SELECT @examSNum = COUNT(*) FROM SC WHERE cno = @cno AND grade IS NOT NULL; -- 平均分(仅计算非 NULL 成绩) SELECT @avgGrade = ISNULL(AVG(CAST(grade AS decimal(5,2))), 0.00) FROM SC WHERE cno = @cno AND grade IS NOT NULL; -- 同步到 CAvgGrade IF EXISTS (SELECT 1 FROM CAvgGrade WHERE cno = @cno) UPDATE CAvgGrade SET Snum = @Snum, examSNum = @examSNum, avgGrade = @avgGrade WHERE cno = @cno; ELSE INSERT INTO CAvgGrade (cno, Snum, examSNum, avgGrade) VALUES (@cno, @Snum, @examSNum, @avgGrade); FETCH NEXT FROM cur INTO @cno; END CLOSE cur; DEALLOCATE cur; END4.2 验证策略:用最小数据集覆盖所有路径
不要等全量数据再测!用 3 行数据即可验证全部逻辑:
| sno | cno | grade |
|---|---|---|
| 95001 | 1 | 85 |
| 95002 | 1 | NULL |
| 95003 | 1 | 92 |
预期CAvgGrade:Snum=3,examSNum=2,avgGrade=88.50。
INSERT新记录('95004','1',65)→Snum=4,examSNum=3,avgGrade=82.33UPDATEgrade=0→examSNum不变(0 是有效成绩)DELETEsno='95001'→Snum=3,examSNum=2,avgGrade=73.50
避坑 / 常见问题 / 排查
现象 1:触发器执行后CAvgGrade数据未更新,但SC表变更成功。
原因:INSERTED/DELETED表为空(如UPDATE语句未真正修改任何行,或WHERE条件不匹配)。
解决:在触发器开头加IF NOT EXISTS (SELECT 1 FROM inserted) AND NOT EXISTS (SELECT 1 FROM deleted) RETURN;现象 2:
AVG()计算结果为NULL,导致avgGrade字段为NULL。
原因:AVG()在空集时返回NULL,未用ISNULL()包裹。
解决:SELECT @avgGrade = ISNULL(AVG(...), 0.00)现象 3:
UPDATE操作触发两次(一次INSERTED,一次DELETED),导致统计值翻倍。
原因:未用UNION合并课号,而是分别处理inserted.cno和deleted.cno。
解决:统一用临时表@cno_list去重汇总。现象 4:触发器内
CURSOR执行缓慢,大数据量时超时。
原因:游标逐行处理,违背集合操作原则。
解决:改用MERGE语句(SQL Server 2008+)替代游标,性能提升 10 倍以上。现象 5:
RAISERROR后应用层未捕获异常,事务继续执行。
原因:未设置SET XACT_ABORT ON,错误后事务仍处于活动状态。
解决:触发器开头强制添加SET XACT_ABORT ON。
5. 用户自定义函数与游标工资调整:批量数据生成与精细化更新
5.1 generateEID:年份+序列号的员工 ID 生成函数
原报告用存储过程generateEID实现,但更规范的做法是创建标量函数,便于在INSERT中直接调用。注意:GETDATE()获取当前年份,RIGHT('0000'+CAST(@i AS VARCHAR),4)补零:
-- 创建标量函数 CREATE FUNCTION dbo.generateEID(@i int) RETURNS char(8) AS BEGIN DECLARE @year char(4) = CAST(YEAR(GETDATE()) AS char(4)); DECLARE @seq char(4) = RIGHT('0000' + CAST(@i AS varchar(4)), 4); RETURN @year + @seq; END -- 使用函数插入 1000 条数据 DECLARE @i int = 1; WHILE @i <= 1000 BEGIN INSERT INTO employee (eID, eName, salary) VALUES ( dbo.generateEID(@i), 'Employee_' + CAST(@i AS varchar(10)), 2000 + FLOOR(RAND() * 3001) -- 2000~5000 ); SET @i = @i + 1; END5.2 游标工资调整:按区间规则批量更新
原报告游标写法基本正确,但存在两个致命缺陷:
FETCH_STATUS=0应为@@FETCH_STATUS = 0(@@是全局变量前缀);WHERE CURRENT OF mycursor在 SQL Server 中仅适用于DECLARE ... FOR UPDATE的游标,而原声明未指定FOR UPDATE,导致语法错误。
修正版(显式FOR UPDATE+ 安全WHERE):
-- 创建可更新游标 DECLARE mycursor CURSOR FOR SELECT sno, salary FROM employee FOR UPDATE OF salary; OPEN mycursor; DECLARE @sno char(10), @salary int; FETCH NEXT FROM mycursor INTO @sno, @salary; WHILE @@FETCH_STATUS = 0 BEGIN IF @salary < 3000 UPDATE employee SET salary = salary + 300 WHERE sno = @sno; ELSE IF @salary < 4000 UPDATE employee SET salary = salary + 200 WHERE sno = @sno; ELSE UPDATE employee SET salary = salary + 50 WHERE sno = @sno; FETCH NEXT FROM mycursor INTO @sno, @salary; END CLOSE mycursor; DEALLOCATE mycursor;注意:游标性能差,1000 行尚可接受,但万级数据务必改用集合更新:
UPDATE employee SET salary = CASE WHEN salary < 3000 THEN salary + 300 WHEN salary < 4000 THEN salary + 200 ELSE salary + 50 END;
6. 实验避坑手册:7 个让西北工业大学学生集体崩溃的血泪现场
6.1 sp_rename 后视图失效:元数据未刷新的静默灾难
现象:sp_rename 'V_SPJ', 'V_SPJ_三建'执行成功,但SELECT * FROM V_SPJ_三建报错“对象名 'V_SPJ_三建' 无效”。
原因:sp_rename只改sys.objects.name,不刷新sys.sql_modules中的依赖引用。原视图定义仍含V_SPJ,SQL Server 编译时找不到该对象。
解决:重编译视图——EXEC sp_refreshview 'V_SPJ_三建';或重建视图(DROP VIEW+CREATE VIEW)。
6.2 jmsearch 加密后无法调试:没有后悔药的硬编码陷阱
现象:CREATE PROCEDURE jmsearch WITH ENCRYPTION后,sp_helptext返回空,想改逻辑只能DROP重建。
原因:WITH ENCRYPTION是单向操作,源码永久丢失。
解决:开发阶段绝不加密!仅在部署到生产库前加密。本地保留.sql文件备份,用 Git 管理版本。
6.3 insert_s 触发器漏写 INSERT:INSTEAD OF 的“不作为”陷阱
现象:执行INSERT INTO SC VALUES (95009,8,98)后,SC表无新增数据,也不报错。
原因:INSTEAD OF触发器必须显式INSERT,否则原操作被完全替代且无动作。
解决:在ELSE分支中补全INSERT INTO SC SELECT ... FROM inserted;。
6.4 dele_s2 级联删除误删全表:IN 运算符遇 NULL 的玄学行为
现象:删除S表某行后,SC表所有记录被清空。
原因:deleted.sno若含NULL(如UPDATE时sno被设为NULL),WHERE sno IN (SELECT sno FROM deleted)因NULL比较恒为UNKNOWN,导致条件不成立,DELETE无WHERE子句变成全表删除。
解决:WHERE sno IN (SELECT sno FROM deleted WHERE sno IS NOT NULL)。
6.5 update_sc_cavggrade 中 AVG(NULL):聚合函数的沉默杀手
现象:某课程无成绩时,avgGrade显示NULL,前端展示为“NaN”。
原因:AVG()对空集返回NULL,未做ISNULL()处理。
解决:SELECT @avgGrade = ISNULL(AVG(CAST(grade AS decimal(5,2))), 0.00)。
6.6 游标CURRENT OF报错:FOR UPDATE 的隐形门槛
现象:UPDATE ... WHERE CURRENT OF mycursor提示“游标未声明为可更新”。
原因:声明游标时未加FOR UPDATE子句。
解决:DECLARE mycursor CURSOR FOR SELECT ... FROM employee FOR UPDATE OF salary;。
6.7 触发器嵌套超限:AFTER 触发器引发连锁反应
现象:dele_s2删除SC行时,触发SC表的其他触发器,导致嵌套层数超 32 限制。
原因:AFTER DELETE中DELETE FROM SC会再次触发自身。
解决:在触发器开头加IF TRIGGER_NESTLEVEL() > 1 RETURN;限制嵌套深度。
从那以后我每次写INSTEAD OF触发器,都强制走一遍SELECT * FROM inserted和SELECT * FROM deleted看数据;每次用sp_rename,必跟一句sp_refreshview;每次加密存储过程前,先sp_helptext备份到 Notepad++。这些动作加起来不到 10 秒,却省去了两小时排查“为什么明明执行成功却没效果”的绝望。希望帮到你。
本文还有配套的精品资源,点击获取