存储过程、触发器和游标
存储过程、触发器和游标是数据库编程常用技术,对数据库应用系统开发意义重大,虽在不同系统中定义有别但原理相似,本章以 SQL Server 2008 为例介绍其概念与使用方法。
5.1存储过程
1、概念
存储过程是数据库编程中的关键概念。它是一组为实现特定功能而组合的 SQL 语句集合。这些语句经编译后存储在数据库内,用户可通过特定调用方式执行。存储过程具有以下特性:
- 名称:方便用户识别与调用。
- 参数:用于接收外部传入数据,增强处理灵活性。
- 返回值:将处理结果反馈给调用者。
- 嵌套调用:一个存储过程可调用另一个存储过程,提升功能的复杂度与灵活性。
2、分类
- 系统存储过程:由数据库系统自带,主要执行系统级操作,例如管理数据库、获取系统信息等。
- 扩展存储过程:允许用户使用外部语言(如 C 语言)编写函数,并集成到数据库中,以实现更复杂功能。
- 用户自定义存储过程:由用户依据自身业务需求编写,用于处理特定业务逻辑,满足个性化需求。不同类型的存储过程在数据库管理与应用开发中均发挥重要作用 。
3、优点
快速执行、安全性好、访问统一、命名代码,允许延迟绑定、减少网络通信流量
4、SQL注入攻击
- SQL 注入攻击原理
SQL 注入攻击是攻击者在应用程序的输入字段中插入恶意 SQL 代码,使应用程序构造出错误的 SQL 语句,从而欺骗数据库执行非预期操作 。当应用程序没有对用户输入进行严格验证和过滤时,攻击者输入的恶意代码就可能被当作 SQL 语句的一部分执行。
- 结合代码示例分析
- 原始代码意图:代码本意是根据用户输入的用户名(
txtusername变量)和密码(txtpsd变量),从users表中查询匹配的用户记录。构造的 SQL 语句如select * from users where username='"+txtusername+"' and psd='"+txtpsd+"',正常情况下,它会筛选出用户名和密码都匹配的用户。 - 恶意输入与攻击过程:图中攻击者输入
123作为用户名,同时在用户名输入框中注入了or 1=1 --。or 1=1是一个恒成立的条件(因为1=1永远为真 ),--是 SQL 中的注释符,用于注释掉后面的内容。这样,原本的 SQL 查询语句拼接后变为select * from users where username='123 or 1=1 --' and psd='123',经数据库解析执行时,--后的and psd='123'被注释掉,实际执行的语句相当于select * from users where username='123' or 1=1,由于or 1=1恒成立,所以会返回users表中的所有记录,攻击者无需正确密码就能绕过登录验证 。
- 可能造成的危害
- 数据泄露:攻击者可获取敏感用户信息,如用户名、密码、个人资料等。
- 非法操作:若攻击者权限足够,可能执行插入、修改、删除数据等操作,破坏数据库的完整性和可用性 。
- 权限提升:进一步获取更高权限,控制数据库甚至服务器,对系统造成更严重破坏。
- 防范措施
- 参数化查询:将 SQL 代码与用户输入分离,使输入仅作为数据处理,而非 SQL 代码的一部分。如在 SQL Server 中使用
SqlParameter类。 - 输入验证和过滤:对用户输入进行严格检查,过滤掉特殊字符和 SQL 关键字,阻止恶意代码注入。
- 最小权限原则:数据库用户仅分配必要的权限,降低攻击造成的损失。
5、存储过程与函数的区别如下:
- 执行效率:存储过程是预编译的,执行效率比函数高 。
- 返回值:存储过程可以不返回任何值,也能通过多个输出变量返回结果;函数则有且必须有一个返回值 。
- 使用方式:存储过程必须单独执行;函数可以嵌入到表达式中,使用更灵活 。
- 应用场景:存储过程主要用于逻辑处理;函数主要用于实现特定功能 。
6、创建存储过程

- 语法格式
CREATE PROCEDURE <过程名>
AS
<SQL语句>
- 示例:在学生 - 课程数据库中创建一个存储过程,检索学分超过 3 的课程名称及选修该课程的学生姓名、所在系和成绩。
CREATE PROCEDURE Query_Grade
AS
SELECT Cname,Sname,Sdept,Grade
FROM Student S,Course C,SC
WHERE S.Sno=SC.Sno AND SC.Cno=C.Cno AND Ccredit>3;
7、执行存储过程
- 语法格式
[EXEC[UTE]][@返回状态变量=]<过程名>
[[@参数1=]值1|@变量1 [OUT[PUT]]
[,[@参数2=]值2|@变量2 [OUT[PUT]][,…]
[WITH RECOMPILE][;]
其中,OUTPUT指定存储过程将值送入输出参数;WITH RECOMPILE表示执行该存储过程时强制重新编译。
- 示例
- 无参数存储过程执行:直接使用
EXEC Query_Grade。 - 带参数存储过程执行:创建带参数的存储过程,通过输入学生姓名,检索该学生的平均成绩。
CREATE PROCEDURE Query_Avggrade
@Sname CHAR(20)
AS
SELECT S.Sno,Sname,Avg(Grade) Avggrade
FROM Student S,SC
WHERE S.Sno=SC.Sno AND Sname=@Sname;
调用方式:
- 使用常量调用:
EXEC Query_Avggrade '赵晨'或EXEC Query_Avggrade @Sname = '赵晨'。 - 使用变量调用:
DECLARE @InputSname CHAR(20)
SELECT @InputSname ='赵晨'
EXEC Query_Avggrade @InputSname
8、修改和删除存储过程

- 修改存储过程
- 语法格式
ALTER PROC[EDURE] <过程名>
[@参数1 数据类型 [=默认值]][OUT|OUTPUT][READONLY]
[,@参数2 数据类型 [=默认值]][OUT|OUTPUT][READONLY][,…]
[WITH ENCRYPTION]
AS {<T-SQL语句>[;]};
其参数及保留字含义与CREATE PROCEDURE相同。
- 删除存储过程
- 语法格式:
DROP PROC[EDURE] <过程名>;。
9、输出raiserror和print
在 SQL Server 中,RAISERROR(在 SQL Server 2012 及之后版本建议使用 THROW)和 PRINT 都可以用来输出信息,但它们的用途和特点有所不同,下面详细介绍:
RAISERROR
RAISERROR 用于生成错误信息并将其返回给应用程序或客户端。它不仅可以输出错误消息,还能指定错误的严重级别和状态,从而影响程序的执行流程。在你的示例中,RAISERROR('该学号不存在选课记录', 16, 1); 用于抛出一个错误信息。
用途
- 错误处理:在存储过程、触发器或批处理中,当遇到不符合业务规则的情况时,可以使用
RAISERROR抛出错误,通知调用者发生了异常。 - 事务回滚:如果错误严重级别设置为 11 及以上,会触发事务回滚,保证数据的一致性。
参数解释
- 第一个参数:是错误消息的文本内容,如
'该学号不存在选课记录'。 - 第二个参数:是错误的严重级别,范围从 0 到 25。16 表示一般错误,通常用于用户定义的错误。
- 第三个参数:是错误的状态码,范围从 1 到 127,用于标识错误发生的位置或上下文。
PRINT
PRINT 用于将文本消息输出到客户端的消息窗口,它不会影响程序的执行流程,也不会触发事务回滚。
用途
- 调试信息输出:在开发和调试过程中,可以使用
PRINT输出中间结果或变量值,帮助你理解程序的执行过程。 - 状态信息提示:在存储过程或批处理中,输出一些操作的状态信息,让用户了解程序的执行进度。
两者区别
| 比较项 | RAISERROR |
PRINT |
|---|---|---|
| 对程序流程的影响 | 严重级别 11 及以上会触发事务回滚,并且可能导致程序中断执行 | 不影响程序的执行流程 |
| 消息类型 | 主要用于输出错误消息,可携带错误级别和状态信息 | 用于输出普通的文本消息 |
| 应用场景 | 用于业务规则检查失败、数据异常等情况,通知调用者发生错误 | 用于调试代码、输出操作状态信息等 |
示例代码
-- 创建一个简单的存储过程,演示 RAISERROR 和 PRINT 的使用
CREATE PROCEDURE CheckStudentCourses
@studentID INT
AS
BEGIN
-- 检查是否存在选课记录
IF NOT EXISTS (SELECT 1 FROM CourseEnrollments WHERE StudentID = @studentID)
BEGIN
-- 使用 RAISERROR 抛出错误
RAISERROR('该学号不存在选课记录', 16, 1);
RETURN;
END
-- 输出操作成功的信息
PRINT '该学号存在选课记录,操作继续...';
-- 这里可以继续执行其他操作
END;
十、存储过程的参数及返回值

-
输入参数
:供调用程序向存储过程传送数据,需定义变量名和类型,可设默认值,值能为常量或变量值。
- 例如,创建存储过程
Query_Avgggrade,通过输入学生姓名(输入参数@Sname)检索其平均成绩,定义如下:
- 例如,创建存储过程
CREATE PROCEDURE Query_Avgggrade
@Sname CHAR(20)
AS
-- 具体查询逻辑
SELECT S.Sno, Sname, Avg(Grade) Avggrade
FROM Student S, SC
WHERE S.Sno = SC.Sno AND Sname = @Sname
--GROUP BY S.Sno,Sname;
- 调用时可用常量,如:
EXEC Query_Avgggrade '赵晨'
- 也可用变量,先声明变量,再赋值,最后执行:
DECLARE @InputSname CHAR(20)
SELECT @InputSname = '赵晨'
EXEC Query_Avgggrade @InputSname
-
输出参数:用
OUTPUT关键字声明,让存储过程能将数据或游标变量传回调用程序。
- 例如,
Query_Grade_OUT存储过程,通过学号查找学生平均成绩并输出,定义如下:
- 例如,
CREATE PROCEDURE Query_Grade_OUT
@Sno CHAR(9),
@AvgGrade SMALLINT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SELECT @AvgGrade = AVG(Grade) FROM SC WHERE Sno = @Sno;
END
- 执行时:
DECLARE @OutAvg SMALLINT
EXEC Query_Grade_OUT '201215121', @OutAvg OUTPUT
SELECT @OutAvg
- 参数传递:
- 按参数位置传递:按参数顺序依次传入值。例如
Query_Student存储过程,查询姓赵学生信息时:
- 按参数位置传递:按参数顺序依次传入值。例如
EXEC Query_Student '', '赵'
- 按参数名字传递:以参数名指定值,参数顺序任意,灵活性高。例如:
EXEC Query_Student @Sname = '赵'
按名字传递参数比按位置具有更大的灵活性,但按位置传递参数速度更快。
存储过程的返回值
-
使用
RETURN语句指定返回代码。返回值在 -1 到 -99 间表示执行未成功;大于 0 或小于 -99 的整数可作自定义返回值,标识不同执行结果。 -
以
Check_Student存储过程为例,根据学号判定学生是否存在,自定义返回值含义为:0 表示学生存在;1 表示学生不存在;2 表示未指定所需参数;3 表示获取数据出错。
- 定义如下:
CREATE PROCEDURE Check_Student
@Sno CHAR(9) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Scount INT;
IF @Sno IS NULL RETURN (2);
SELECT @Scount = COUNT(*) FROM Student WHERE Sno = @Sno;
IF @@ERROR<>0 RETURN(3);
IF @Scount>0 RETURN(0) ELSE RETURN(1);
END
- 执行代码通过判断返回值输出对应信息:
DECLARE @Sno CHAR(9), @nRtn INT
SELECT @Sno = '201215128'
EXECUTE @nRtn = Check_Student @Sno
IF @nRtn = 0
PRINT '学号为'+@Sno+'的学生存在!'
ELSE IF @nRtn = 1
PRINT '学号为'+@Sno+'的学生不存在!'
ELSE IF @nRtn = 2
PRINT '必需输入学号!'
ELSE IF @nRtn = 3
PRINT '获取数据出错!'
ELSE
PRINT '其他错误!'
5.2触发器
1. 触发器基础
- 定义:触发器是一种特殊的存储过程,当在指定的数据表中对数据进行插入、修改以及删除操作时,会自动执行对应的触发器代码。它为数据库提供了有效的监控和处理机制,确保数据和业务的完整性。
- 分类
- 按触发事件分类
- DML 触发器:响应
INSERT、DELETE、UPDATE操作,用于在数据操作时自动执行特定逻辑。 - DDL 触发器:响应数据定义语言操作,如创建、修改或删除数据库对象等。
- 登录触发器:在用户登录 SQL Server 实例时触发。
- DML 触发器:响应
- 按触发执行方式分类
- AFTER 触发器(后触发器):在触发事件成功执行后触发,可用于数据验证、日志记录等。
- INSTEAD OF 触发器(前触发器):替代触发事件执行,常用于视图等不支持直接 DML 操作的对象上。
- 按触发事件分类
2. 触发器的优缺点
- 优点
- 强化约束功能:能实现比普通约束更复杂的数据完整性规则,如跨表数据验证。
- 跟踪数据变化:方便记录数据的变更情况,可用于审计等场景。
- 支持级联运行:一个触发器执行后可触发其他相关操作,实现复杂业务逻辑。
- 可调用存储过程:在触发器中可调用存储过程,进一步拓展功能。
- 局限性
- 性能问题:每次触发事件发生都要执行触发器代码,可能导致性能下降。
- 维护困难:过多或不恰当使用触发器,会使数据库逻辑复杂,难以理解和维护。
3. SQL SERVER DML 触发器
- INSERT 触发器:在数据插入表或视图时执行。新插入的数据行同时会插入到
inserted虚拟表,该表保存插入数据行副本,用于对比插入前后数据变化。 - DELETE 触发器:执行删除操作时触发。被删除的数据先存放在
deleted虚拟表,用于引用已删除数据,deleted表和原表通常无相同行。 - UPDATE 触发器:数据修改时触发。处理分两步,原始数据移到
deleted表,修改后数据插入inserted表,触发器据此检查并更新数据表。
4. 触发器创建语法总结
在 SQL Server 中,创建触发器的基本语法如下:
CREATE TRIGGER [schema_name.]trigger_name
ON {table_name | view_name}
[WITH ENCRYPTION]
{FOR | AFTER | INSTEAD OF} {[INSERT] [, ] [UPDATE] [, ] [DELETE]}
AS
BEGIN
-- 具体的 T - SQL 操作语句
-- 可包含多条语句,用于实现特定逻辑
END;
[schema_name.]trigger_name:指定触发器的名称,schema_name为架构名,可省略。ON {table_name | view_name}:指定触发器作用的表或视图。[WITH ENCRYPTION]:可选参数,对触发器定义进行加密。{FOR | AFTER | INSTEAD OF}:确定触发时机和方式。FOR和AFTER表示在触发操作成功执行后触发;INSTEAD OF表示替代触发操作执行。{[INSERT] [, ] [UPDATE] [, ] [DELETE]}:指明触发触发器的操作类型,可多选。AS之后的BEGIN...END块内编写具体执行的 T - SQL 代码。
5. 示例及解析
5.1. 维护选课人数一致性(DML 触发器)
-- 创建用于记录课程号及其选课人数的表
CREATE TABLE Course_Num(
Cno CHAR(4) PRIMARY KEY,
Ccount SMALLINT
);
-- 初始化表数据
INSERT INTO Course_Num
SELECT Cno, COUNT(*) FROM SC GROUP BY Cno;
-- 创建触发器
CREATE TRIGGER Tri_Course_Count
ON SC
FOR INSERT, DELETE, UPDATE
AS
BEGIN
UPDATE Course_Num
SET Ccount = (SELECT COUNT(*) FROM SC WHERE SC.Cno = Course_Num.Cno)
WHERE Cno IN (SELECT Cno FROM deleted) OR Cno IN (SELECT Cno FROM inserted);
END;
解析:
- 先创建
Course_Num表并初始化数据。 Tri_Course_Count触发器作用于SC表,当SC表发生INSERT、DELETE、UPDATE操作时触发。- 触发器内部通过
UPDATE语句,依据inserted(插入或更新后的数据)和deleted(删除或更新前的数据)虚拟表中的课程号,重新统计并更新Course_Num表中的选课人数,保证数据一致性。
5.2. 限制成绩非空时删除选课记录(DML 触发器)
CREATE TRIGGER Tri_SC_Del
ON SC
AFTER DELETE
AS
BEGIN
IF EXISTS (SELECT * FROM deleted WHERE Grade IS NOT NULL)
BEGIN
PRINT '成绩非空,不能删除!';
ROLLBACK TRANSACTION;
END
END;
解析:
Tri_SC_Del触发器在SC表执行DELETE操作之后触发。- 利用
IF EXISTS语句检查deleted虚拟表中是否存在成绩非空的记录。若存在,使用PRINT输出提示信息,并用ROLLBACK TRANSACTION回滚事务,撤销删除操作,维护数据完整性。
5.3. 年龄修改约束(DML 触发器)
CREATE TRIGGER Tri_Student_Update
ON Student
AFTER UPDATE
AS
BEGIN
IF UPDATE(Sage)
IF EXISTS (SELECT I.* FROM inserted I, deleted D WHERE I.Sno = D.Sno AND I.Sage < D.Sage)
BEGIN
PRINT '新年龄小于原来年龄,不能修改!';
ROLLBACK TRANSACTION;
END
END;
解析:
Tri_Student_Update触发器作用于Student表,在UPDATE操作之后触发。UPDATE(Sage)函数判断是否对年龄字段Sage进行了修改。若修改,进一步检查inserted虚拟表(更新后数据)中同学生的新年龄是否小于deleted虚拟表(更新前数据)中的原年龄。若满足条件,输出提示信息并回滚事务,实现年龄修改的业务规则约束。
5.4. 基于视图更新(INSTEAD OF 触发器)
-- 创建视图
CREATE VIEW V_Birth(Sno, Sname, BirthYear)
AS
SELECT Sno, Sname, YEAR(GETDATE()) - Sage FROM Student;
-- 创建 INSTEAD OF 触发器
CREATE TRIGGER Tri_VBirth
ON V_Birth
INSTEAD OF UPDATE
AS
BEGIN
IF UPDATE(BirthYear)
BEGIN
UPDATE Student
SET Sage = YEAR(GETDATE()) - I.BirthYear
FROM inserted I
WHERE I.Sno = Student.Sno;
END
END;
解析:
- 先创建
V_Birth视图,视图中通过当前年份减去学生年龄计算出生年份。 Tri_VBirth是INSTEAD OF触发器,作用于V_Birth视图。当尝试更新视图中的出生年份时,触发器触发。- 触发器内部通过
UPDATE语句,根据inserted虚拟表(更新后的数据)中的信息,更新基表Student中的年龄字段,解决视图有导出列时无法直接更新的问题。
5.3游标
5.3.1、游标定义
游标(Cursor)是关系数据库中处理结果集的一种机制,用于支持对结果集的逐行操作。在 SQL Server 中,游标本质上是一个指向查询结果集的指针,允许应用程序对结果集中的每一行数据进行单独处理,弥补了 SQL 语句一次处理整个行集的局限性。
5.3.2、核心作用
- 逐行处理数据
对结果集中的每行数据进行独立操作(如更新、删除或复杂计算),适用于需要逐行遍历的业务场景(如报表生成、数据校验)。 - 定位数据行
在结果集中定位特定行,支持向前、向后或随机滚动(需定义为可滚动游标)。 - 支持数据修改
通过游标直接修改基表数据(需声明为可更新游标),实现定位更新或删除。 - 处理复杂逻辑
在存储过程、触发器中结合循环结构(如WHILE)处理多行数据,扩展 SQL 的编程能力。
5.3.3、游标分类
- 按滚动特性分类
- 静态游标(Static Cursor)
生成游标时将结果集静态存储,后续对基表的修改不会反映到游标中。 - 动态游标(Dynamic Cursor)
实时反映基表数据的变化,游标中的数据会随基表更新而动态调整。 - 键集驱动游标(Keyset-Driven Cursor)
基于唯一标识(主键)定位行,结果集行数固定,但行数据可能随基表更新而变化。 - 快速前向游标(Fast-Forward Cursor)
仅支持向前滚动,性能较高,是默认的游标类型。
- 静态游标(Static Cursor)
- 按更新特性分类
- 只读游标(READ ONLY)
禁止通过游标修改基表数据。 - 可更新游标(UPDATE)
允许使用WHERE CURRENT OF子句对游标当前行进行更新或删除。
- 只读游标(READ ONLY)
5.3.4、游标操作步骤
-
声明游标(DECLARE)
定义游标名称、关联的查询语句及特性(如滚动性、更新性)。
语法示例(T-SQL):DECLARE @cursor_name CURSOR [LOCAL|GLOBAL] -- 作用域(局部/全局) [FORWARD_ONLY|SCROLL] -- 滚动方向(仅向前/可滚动) [STATIC|KEYSET|DYNAMIC|FAST_FORWARD] -- 类型 [READ_ONLY|SCROLL_LOCKS|OPTIMISTIC] -- 更新特性 FOR <SELECT语句>; -
打开游标(OPEN)
执行查询并填充结果集,此时游标指向结果集的第一行之前。OPEN @cursor_name; -
提取数据(FETCH)
使用FETCH语句逐行获取数据,并通过全局变量@@FETCH_STATUS检查操作状态:0:成功-1:超出结果集范围-2:行已删除
滚动操作示例:
FETCH NEXT FROM @cursor_name; -- 下一行(默认) FETCH PRIOR FROM @cursor_name; -- 上一行 FETCH ABSOLUTE 5 FROM @cursor_name; -- 绝对定位第5行 FETCH RELATIVE -2 FROM @cursor_name; -- 相对当前行向前2行 -
处理数据
对提取的行数据进行业务逻辑处理(如计算、更新基表)。 -
关闭游标(CLOSE)
释放游标占用的系统资源,但保留游标定义,可通过OPEN重新激活。CLOSE @cursor_name; -
释放游标(DEALLOCATE)
永久删除游标,释放内存空间。DEALLOCATE @cursor_name;
5.3.5、典型应用场景
-
逐行数据更新
例如,根据学生所在系别调整成绩(如 CS 系加 5 分,IS 系加 10 分)。DECLARE @sno CHAR(9), @dept CHAR(2); DECLARE cur_Student CURSOR FOR SELECT Sno, Sdept FROM Student; OPEN cur_Student; FETCH NEXT FROM cur_Student INTO @sno, @dept; WHILE @@FETCH_STATUS = 0 BEGIN UPDATE SC SET Grade = Grade + CASE @dept WHEN 'CS' THEN 5 WHEN 'IS' THEN 10 ELSE 15 END WHERE Sno = @sno; FETCH NEXT FROM cur_Student INTO @sno, @dept; END; CLOSE cur_Student; DEALLOCATE cur_Student; -
报表生成与统计
遍历结果集并拼接成特定格式的报表数据(如按序号输出课程信息)。 -
数据校验与业务规则
逐行验证数据完整性(如检查订单金额是否超过信用额度)。
5.3.6、优缺点分析
- 优点
- 灵活性高:支持逐行处理和复杂逻辑。
- 兼容性强:适用于无法用集合操作完成的场景。
- 缺点
- 性能开销大:逐行操作效率低于集合操作,尤其对大数据集。
- 维护复杂:游标代码可读性较差,易引发资源泄漏(如未关闭或释放游标)。
- 阻塞问题:默认情况下,游标会锁定数据行,可能影响并发性能。
5.3.7、最佳实践
- 优先使用集合操作
能用UPDATE/DELETE配合WHERE子句完成的操作,避免使用游标。 - 限制结果集大小
在声明游标时通过WHERE子句过滤数据,减少处理行数。 - 选择合适的游标类型
- 仅向前滚动:使用
FAST_FORWARD(性能最优)。 - 只读场景:声明为
READ ONLY。 - 需滚动:启用
SCROLL并谨慎使用定位操作。
- 仅向前滚动:使用
- 及时释放资源
确保每次使用游标后执行CLOSE和DEALLOCATE,避免内存泄漏。
5.3.8、游标与存储过程 / 触发器的结合
游标常嵌入存储过程或触发器中,用于处理需要逐行逻辑的业务。例如:
- 在存储过程中遍历订单数据,计算累计金额。
- 在触发器中逐行验证插入数据的合法性(如库存是否充足)。
通过合理使用游标,可以在保持数据库性能的前提下,实现复杂的业务逻辑。
「智能机器人开发者大赛」官方平台,致力于为开发者和参赛选手提供赛事技术指导、行业标准解读及团队实战案例解析;聚焦智能机器人开发全栈技术闭环,助力开发者攻克技术瓶颈,促进软硬件集成、场景应用及商业化落地的深度研讨。 加入智能机器人开发者社区iRobot Developer,与全球极客并肩突破技术边界,定义机器人开发的未来范式!
更多推荐



所有评论(0)