☰
存储过程触发器游标实战:SQL Server数据库实验避坑指南
2026/10/13 20:06:42 网站建设 项目流程

简介:这是一份西北工业大学《数据库原理》课程实验报告五的完整文档,主要面向学习数据库原理与应用的高校学生,尤其是西工大相关课程的同学。报告围绕数据库基本操作展开,涵盖数据表创建与管理、系统存储过程调用、自定义存储过程编写与执行,以及多种触发器的创建与验证,并附带完整SQL语句和运行截图,便于对照练习。压缩包内仅包含1个doc文件,大小约231KB,内容为11页的实验报告正文,步骤清晰、结果直观。目前已有716人学习下载。通过本报告,读者可以掌握sp_rename等系统存储过程的使用方法,理解带参数存储过程、加密存储过程的编写思路;同时能学会INSERT、DELETE、UPDATE三类触发器的设计逻辑,例如禁止删除、级联删除、禁止更新字段等典型场景,并了解触发器禁用、删除及自动维护统计表的综合应用。对于需要完成数据库实验或准备相关考试的学生,这份报告可直接作为参考模板,也可帮助加深对数据库完整性和并发控制的理解。

1. 一份西工大数据库实验报告:存储过程、触发器、游标全流程实战

拿到这份西北工业大学《数据库原理》实验报告时,我最先翻的其实不是答案,而是它的实验任务顺序:改视图名、建带参存储过程、连做六个触发器、最后用游标批量调工资。作为把同一套实验在 SQL Server 上跑过不止一遍的人,我的判断是,这份资料的价值不在“标准答案”,而在于它把存储过程、触发器、游标这三块最容易写错又不报错的内容,全部放在了同一套数据库环境下让你反复练手。互联网上能找到的数据库实验报告不少,但多数只贴代码不解释选型,这份却把验证步骤和踩坑点都留了痕迹。正在做数据库课程设计、准备期末上机的同学,或者想把手写 SQL 落到真实调试场景的工程师,都值得照着敲一遍。

2. 存储过程三板斧:带参查询、加密保护与文本校验

2.1 带参存储过程:为什么 jsearch 不能拿裸 SQL 凑合

实验第一题要求创建一个叫 jsearch 的存储过程:输入任意工程代号,返回供应商名称、零件名称和工程名称。很多新手觉得这题多余——直接在查询分析器里敲一条 SELECT 不好吗?但带参存储过程的价值,在 jsearch 这种“参数固定、结构固定”的调用场景里体现得最清楚:SQL 文本和传入值分离,每次执行传入不同参数,数据库会复用执行计划,同时避免了把用户输入直接拼进 SQL 字符串。

create procedure dbo.jsearch @search_jno nchar(20) as begin select j.jname, s.sname, p.pname from dbo.s as s inner join dbo.spj on spj.sno = s.sno inner join dbo.p as p on spj.pno = p.pno inner join dbo.j as j on spj.jno = j.jno where spj.jno = @search_jno; end go exec dbo.jsearch @search_jno = N'J1';

这里有一个原报告里很容易连累初学者的小坑:报告原文写的是create procedure jsearch(search_jno nchar(20)),参数不带@也没有类型前缀,这在 T-SQL 语法里直接报错。SQL Server 的存储过程参数必须用@开头,变量声明同理,写成上面的@search_jno nchar(20)才能编译通过。字段是 nchar,执行时传值加N前缀,能避免本地连接和远程连接之间因为字符编码不同而产生的乱码。查询用四表 join,从spj作为关联骨架开始,分别挂上s、p、j,本质上是在做“供应关系 → 供应商 → 零件 → 工程”的逐级展开,这种从中间关系表往外扩散的写法,在带参存储过程里是最常见的结构。

2.2 WITH ENCRYPTION 与 sp_helptext:加密存储过程的验证方式

第二题要求用一个 S 表创建加密的存储过程 jmsearch,返回供应商所有信息。“加密”在这个实验里的真实含义,是 SQL Server 的WITH ENCRYPTION选项,创建时把存储过程的定义文本做混淆处理,之后系统目录里保存的是密文。注意,这个“加密”保护的是存储过程源码,而不是业务数据;业务字段该明文存还是明文存,两部分是两回事。

create procedure dbo.jmsearch with encryption as begin select * from dbo.s where city = N''; end go exec dbo.jmsearch; go exec sp_helptext 'dbo.jmsearch';

sp_helptext是 SQL Server 查看对象定义的系统存储过程,对普通存储过程返回完整创建语句,对WITH ENCRYPTION创建的对象,返回空结果加一条提示信息。所以验证“是否真的加密了”,最直接的观测点就是sp_helptext的输出是否为 NULL 或空文本。下表是我平时判断对象可读性的参照:

对象状态sp_helptext 输出能否直接查到源码
普通存储过程返回完整定义文本能
WITH ENCRYPTION 存储过程返回空/NULL,附提示不能
视图/触发器(普通)返回定义文本能
WITH ENCRYPTION 视图返回空/NULL不能

报告里的where city = N''看起来有点怪,它只是想筛出城市字段为空的供应商,作为演示够用,实际业务里这个条件大概率要换成具体城市名。还需要记住一点:WITH ENCRYPTION只在创建时生效,创建成功后再执行ALTER重新定义,密文会被新定义覆盖,所以“加密”不是长期免维护的保险柜。

2.3 查看、执行、删除:存储过程生命周期管理

报告在完成 jsearch 和 jmsearch 之后,依次做了三件事:sp_helptext查看文本、exec jmsearch执行、drop procedure jmsearch删除。这个顺序本身就是标准操作流程,我维护线上数据库也按同样的顺序走,只是多了一步幂等清理。

exec sp_helptext 'dbo.jsearch'; exec sp_helptext 'dbo.jmsearch'; go exec dbo.jmsearch; go drop procedure dbo.jmsearch; go if object_id('dbo.jmsearch', 'p') is not null drop procedure dbo.jmsearch;

sp_helptext适合在改存储过程之前留一份“当前版本”存档;drop procedure会把对象定义从sys.sql_modules里一并清掉。如果脚本要重复执行,建议先写if object_id(...) is not null drop procedure ...做幂等清理,否则第二次运行会报“已存在同名对象”。

3. 触发器不是黑匣子:INSTEAD OF 与 AFTER 的行为差

3.1 用 INSTEAD OF 说“不”,用 AFTER 做“善后”

报告里有一组非常典型的对照实验:insert_s是instead of insert,dele_s2是after delete。同样是“触发器”三个字,两种触发时机语义完全不同。instead of的意思是“用触发器里的代码取代原本的 INSERT 操作”,适合做校验和拦截,比如向 SC 表插入记录时,如果课程号在 C 表中不存在,就打印提示并回滚。after的意思是“原本的 INSERT/DELETE/UPDATE 已经执行,再接着去做附加动作”,适合做级联和统计,比如删除 S 表学生时,顺带把 SC 表里这个学生的选课记录也删掉。

这两种选择是触发器设计里最前置的决策。需要拦截非法操作时,优先考虑instead of,因为它在数据变更发生前就介入;需要在合法变更之后维持其他表的一致状态(统计、同步、归档、日志),用after更贴合执行顺序。

create trigger insert_s on dbo.sc instead of insert as begin if exists ( select 1 from inserted as i where i.cno not in (select cno from dbo.c) ) begin raiserror(N'不能插入这样的记录!!', 16, 1); rollback transaction; end else begin print N'记录插入成功!!'; insert into dbo.sc (sno, cno, grade) select sno, cno, grade from inserted; end end

这段代码里有一个隐患需要点名:raiserror之后的rollback transaction不能随便删,因为raiserror只是把错误抛给客户端,事务状态并不会自动回滚。实验验证时要分别插入一条合法记录和一条非法记录,观察提示信息是不是按预期分支走。实际项目里我更习惯用throw直接抛出异常,让应用层捕获后统一处理,而不是在触发器里散落print字符——print的提示只出现在 SSMS 的消息页签里,应用程序基本感知不到。

3.2 inserted 与 deleted:触发器里的影子表

inserted和deleted是触发器内部的两张内存表,这是整个实验里最少被真正讲透、却最影响结果的机制。inserted保存“这次操作影响的新行”,deleted保存“这次操作影响的旧行”。INSERT 只产生inserted;DELETE 只产生deleted;UPDATE 则两张表都有,deleted放更新前的旧版本,inserted放更新后的新版本。

create trigger dele_s1 on dbo.s instead of delete as begin rollback transaction; print N'禁止删除'; end go create trigger dele_s2 on dbo.s after delete as begin delete from dbo.sc where sno in (select sno from deleted); end

两个触发器放在一起对比非常清楚:dele_s1用instead of delete直接rollback,实现“禁止删除”,S 表的物理数据没有任何变化;dele_s2用after delete,在 S 表记录已经删除之后,从deleted表里取出被删学生的 sno 集合,去 SC 表做级联清理。这里in (select sno from deleted)是触发器里最常用的写法,为的是处理“一次删除多条记录”的场景。如果写成where sno = (select sno from deleted),SQL Server 在多行删除时会直接报“子查询返回多行”的错误。

这里还有一个报告里没提到的坑:dele_s1和dele_s2在同一张表上同时存在时,instead of delete会把所有 DELETE 操作拦下来,after delete触发器永远没有机会触发——因为根本没有任何行被真正删除。所以实验里这两题虽然是分开验证的,但如果你把两个触发器同时留在 S 表上再跑一次删除,会发现 SC 表没有被级联清理。这个现象我在下面避坑章节会再展开。

3.3 禁用触发器与删除触发器:留后路与断后路

报告里有一道操作题很耐看:创建update_s触发器禁止修改 S 表的sdept字段;验证生效之后,用disable trigger禁用,再更新sdept验证解禁成功;最后drop trigger把触发器彻底删掉。

create trigger update_s on dbo.s instead of update as begin if update(sdept) begin raiserror(N'sdept 不能被修改', 10, 1); end end go disable trigger update_s on dbo.s; go update dbo.s set sdept = N'CS1' where sno = N'95002'; go drop trigger update_s; go

disable trigger和drop trigger的区别,一句话概括:禁用是“留后路”,触发器定义还在,随时可以重新启用,适合在批量导入数据或临时放行时用;删除是“断后路”,对象没了,要恢复只能重新创建。报告里先 disable 再 drop 的顺序,是想让学生体会两种操作对“后续还能不能自动拦截”的不同影响。在正式环境里,我一般只用disable trigger做临时放行,并在维护窗口结束时重新enable trigger,不会随便drop——因为 drop 之后一旦应用层某个功能依赖了触发器的拦截逻辑,就相当于把安全网给撤了。

4. CAvgGrade 统计表:触发器里的多行与 NULL、0 边界

4.1 统计表为什么要交给触发器维护

第四题要求在 Student 库里新建一张成绩统计表 CAvgGrade,并利用触发器实现:SC 表插入、删除、成绩更新时,自动更新统计表。这道题的真实工作场景非常常见——课程平均分、选课人数这类指标,如果每次展示都由应用层现场count(*)和avg(grade),数据量一大,查询成本成倍上涨。所以很多系统会单独维护统计表,问题是谁来保证统计表和明细表始终同步。

答案是触发器。把统计逻辑放在for insert, delete, update的触发器里,应用层每次对 SC 表做增删改,统计表都会自动重算,应用层完全无感。相比应用层在事务里“先改 SC 再改 CAvgGrade”的做法,触发器更不容易漏更新,也不存在“上层代码忘记调统计接口”的人为失误。

4.2 原报告统计逻辑分析:变量承接的隐患

原报告给出的触发器核心代码是这样一个思路:从inserted或deleted里取出当前被影响的课程号,然后对 SC 表做统计,再update CAvgGrade。

create trigger update_sc_cavggrade on dbo.sc for insert, delete, update as begin declare @cno nchar(10); declare @ssum int; declare @examssum int; declare @avggrade numeric(10, 2); select @cno = cno from inserted; select @cno = cno from deleted; select @ssum = count(*) from sc where cno = @cno; select @examssum = count(*) from sc where cno = @cno and grade >= 0; select @avggrade = avg(grade) from sc where cno = @cno and grade >= 0; update CAvgGrade set ssum = @ssum, examSnum = @examssum, avgGrade = @avggrade where cno = @cno; end

先说结论:这个写法在“一次只插入或删除一行”的验证场景下能跑,但它有一个隐蔽问题——select @cno = cno from inserted和select @cno = cno from deleted都只承接了第一个变量赋值。如果应用层一次性插入 10 条不同课程的成绩,@cno只会得到其中一行,count和avg也只按这一行算,其余课程的统计表就漏更了。稳妥做法是先按 cno 分组,再把各组结果合并进统计表。

create trigger update_sc_cavggrade_fixed on dbo.sc for insert, delete, update as begin set nocount on; merge into dbo.cAvgGrade as t using ( select sc.cno, count(*) as ssum, sum(case when sc.grade is not null then 1 else 0 end) as examssum, avg(sc.grade) as avggrade from dbo.sc where exists ( select 1 from (select cno from inserted union select cno from deleted) as d where d.cno = sc.cno ) group by sc.cno ) as s on t.cno = s.cno when matched then update set t.ssum = s.ssum, t.examSnum = s.examssum, t.avgGrade = s.avggrade when not matched then insert (cno, ssum, examSnum, avgGrade) values (s.cno, s.ssum, s.examssum, s.avggrade); end

这个修正方案有两个核心点。第一,用union合并inserted和deleted的课程号,避免“插入时 deleted 为空、删除时 inserted 为空”导致取不到完整课程集合。第二,用merge把重算结果一次性写回统计表,when matched更新已有课程,when not matched插入新出现课程,SC 表里课程被全部删光时统计表也能跟着收敛。实际跑实验的时候,建议先插入一行新课程,再删掉一行旧课程,最后更新一个已有学生的成绩,三个动作分别观察统计表的数字变化,才能把触发器的三种触发路径都覆盖到。

4.3 NULL 与 0 的边界:考试人数和平均分的口径

这道题最后一个容易做错的点,是成绩为空和成绩为零的处理。报告明确写了:grade 为 NULL 表示未参加考试,计算平均分时不算;grade 为 0 表示考了但得零分,必须算进去。如果用where grade > 0来过滤,就把 0 分考生漏掉了,平均分被抬高;如果用where grade is not null,则既排除了 NULL 也保留了 0 分,口径才对得上。

select cno, count(*) as ssum, sum(case when grade is not null then 1 else 0 end) as examssum, avg(grade) as avggrade from sc group by cno;

avg函数对 NULL 自动忽略,所以这里直接avg(grade)就天然满足“不计 NULL、计 0 分”的要求,前提是 SC 表里的 grade 字段允许 NULL,并且应用层严格用 NULL 表示未考试。验证时,要往 SC 表插入一个grade = NULL的学生和一个grade = 0的学生,观察统计表里考试人数和平均分的变化差异——只有这两种数据都覆盖到,验证才算完整。

5. 避坑排查:这份实验最容易翻车的五个现场

5.1 现象:sp_rename 之后视图名找不到

创建完视图 V_SPJ,执行exec sp_rename 'v_spj', 'v_spj_三建'后,再查询视图报“对象名无效”。

原因:sp_rename的第一个参数要写对象当前的完整名称,包括架构名;视图重命名后,如果后续脚本仍引用旧名称,立即报错。报告里的v_spj_三建含中文字符,在 SQL Server 里能建,但客户端连接字符集不对时,查询时会变成乱码或找不到对象。

解决:重命名时写完整名称,并在同一次会话中立即用新名验证:

exec sp_rename 'dbo.v_spj', 'v_spj_三建'; go select * from dbo.v_spj_三建;

注意:中文字段名和对象名在 SQL Server 里能建,但跨数据库、跨工具迁移时容易踩编码坑,正式环境尽量避免。

5.2 现象:INSTEAD OF 触发器里 rollback 后,客户端报“事务计数不匹配”

向 SC 表插入非法课程号,触发器确实打印了“不能插入”,但应用端报错事务计数不匹配,程序崩溃。

原因:rollback transaction在触发器中会回滚整个事务,批处理随后会继续跑,让客户端的事务计数和服务器端不一致,SQL Server 报 “Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements”。

解决:在instead of触发器中不要靠 rollback 来“拒绝”,直接在分支里判断后返回并抛出异常,让应用层处理,而不是用回滚手段去干扰事务状态。上面的insert_s示例里,把rollback transaction换成throw或raiserror后,再配合return退出,就不会出现事务计数错乱。

5.3 现象:CAvgGrade 多行插入统计漏行

应用一次性插入多条不同课程的成绩,统计表只有一条课程被更新。

原因:原报告写法用select @cno = cno from inserted承接,变量只保留一行结果,其余课程号被覆盖,统计自然不全。

解决:按第 4 章的merge写法,以inserted union deleted作为分组骨架,按 cno 分组统计后整体写回。如果因为某些原因不能改触发器结构,至少也要用select distinct cno from inserted开一个游标逐条处理,但不推荐,性能差且容易被再次误解。

5.4 现象:generateEID 只插入 1 条数据就“成功”了

创建完 generateEID 存储过程,执行后 employee 表只有 1 行数据,而不是 1000 行。

原因:原报告的 while 循环里,return写在了循环体内,存储过程第一次迭代后就直接返回,后面的 999 条插入根本没机会执行。

create procedure dbo.generateEID as begin set nocount on; declare @i int = 0; while (@i < 1000) begin insert into dbo.employee (eid, ename, salary) values (20160001 + @i, N'name' + convert(nvarchar(20), @i), 2000 + cast(floor(rand() * 3001) as int)); set @i = @i + 1; end end go exec dbo.generateEID;

return的位置是重点,必须放在while循环之外或整个存储过程末尾,不能放在循环体内。生成员工 ID 的逻辑是 8 位数字,前四位 2016,后四位从 0001 递增,对应20160001 + @i的写法,代入 i=0 得到 20160001,i=999 得到 20161000,正好覆盖报告要求的 1000 条。工资用2000 + cast(floor(rand() * 3001) as int),rand() 返回 0 到 1 之间的浮点数,乘 3001 后取 floor,得到一个 0 到 3000 的整数,加上 2000 后正好落在 2000 到 5000 区间。注意rand()在循环里每次都会重新计算,所以这 1000 条工资不会重复。

5.5 现象:游标循环卡死,工资被重复涨

执行游标存储过程调整工资,循环一直不结束,或员工工资被重复更新,涨完一轮又涨一轮。

原因:游标循环里没有正确推进fetch next,或者while条件写成了while @@FETCH_STATUS <> -1之类的错误判断,导致指针永远停在第一行,不断对同一行执行update ... where current of。

解决:游标循环的固定骨架是“open → fetch → while @@FETCH_STATUS = 0 → 业务处理 → fetch next → close → deallocate”:

declare mycursor cursor local for select salary from dbo.employee; open mycursor; declare @salary int; fetch next from mycursor into @salary; while @@fetch_status = 0 begin if @salary < 3000 update dbo.employee set salary = salary + 300 where current of mycursor; else if @salary < 4000 update dbo.employee set salary = salary + 200 where current of mycursor; else update dbo.employee set salary = salary + 50 where current of mycursor; fetch next from mycursor into @salary; end close mycursor; deallocate mycursor;

current of mycursor要求更新语句作用在游标当前行上,不能用子查询或 join 绕过;@@fetch_status = 0是继续循环的条件,-1 表示已超出结果集,-2 表示行已被删除。注意工资增长规则是有边界区间的:低于 3000 涨 300,3000 到 4000 之间涨 200,大于等于 4000 涨 50。代码里if @salary < 3000和else if @salary < 4000的组合,恰好把 3000 这个临界值划到了第二档,和题目规则一致。实际业务里,这种按区间更新的需求完全可以用一条集合式 UPDATE 替代游标,性能高得多,游标只是用来理解逐行处理的教材手段。

6. 把这套实验变成你自己的 SQL 调试模板

实验报告做完了,更值钱的是把它的验证流程沉淀成习惯。我每次在 SQL Server 里写存储过程或触发器,都会强制自己走一套固定动作:创建前先写幂等清理,创建后立刻查sys.sql_modules看定义是否正常注册,再执行一次针对性的增删改查触发验证,最后清场。

select o.name, m.definition from sys.objects as o left join sys.sql_modules as m on o.object_id = m.object_id where o.name in ('jsearch', 'jmsearch', 'insert_s', 'dele_s2');

这组查询能一次性看到对象是否存在、定义是否加密、源码文本是什么。存储过程或触发器创建时报“对象已存在”,先跑这条 SQL 确认状态,比反复猜测快得多。

拿这份报告来说,每个触发器建完,我都会做三个验证动作:插入一条合法数据看是否成功入库;插入一条非法数据看是否被拦截并给出明确提示;删除一行数据看级联操作是否生效。三次验证都通过,这张表相关的触发器才算真正可用。触发器是否被禁用,也可以查询sys.triggers里的is_disabled字段,而不是靠肉眼去 SSMS 里翻树节点。

从那以后,我每拿到一份线上发来的“触发器失灵”问题,都会先问自己三个问题:这个触发器是 INSTEAD OF 还是 AFTER?我有没有真的执行过 DELETE 之外的操作来验证deleted表的内容?统计口径里 NULL 和 0 是不是被同一个条件误伤?这套实验报告让我把这些追问变成了肌肉记忆,希望你照着跑一遍时,也能把同样的验证顺序刻进自己的习惯里。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询