用 RAISERROR 抛出自定义错误
📝 微软官方推荐新项目优先使用
throw
,因为它行为更一致、更简单、更贴近编程语言(如 Java、JS 等)的异常处理习惯。
raiserror
(注意:SQL Server 从 2012 起推荐使用 throw
)是 T-SQL(Transact-SQL)的「关键字」,用于生成自定义错误,但不一定中断当前 T-SQL 语句批处理(Batch)的执行。
- Batch(批处理单元): 一次性提交到 SQL Server 的一组 T-SQL 语句,它的边界由
go
分隔- 在 SSMS(SQL Server Management Studio)、sqlcmd(SQL Server 命令行工具)、 IntelliJ IDEA 的 Database Plugin 等工具中,
go
用于分隔 Batch - 每个 Batch 包含从上一个
go
(或脚本开头)到下一个go
(或脚本结尾)之间的所有 T-SQL 语句 - 如果没有
go
,整个脚本被视为单个 Batch
- 在 SSMS(SQL Server Management Studio)、sqlcmd(SQL Server 命令行工具)、 IntelliJ IDEA 的 Database Plugin 等工具中,
go
(批处理分隔符):SQL Server 相关工具的批处理分隔符,不属于 SQL 语言本身,也不是数据库引擎识别的语法或语句,常用于分隔无法在同一 Batch 中执行的 SQL- 🚨 存储过程、函数、触发器、视图等对象的定义语句,必须放在独立的 Batch 中,即需出现在上一个
go
之后(或脚本开头),并以go
结束(或脚本结尾) - 批处理级作用域:所有在
go
之前声明的局部变量、临时表或上下文信息,仅在当前批次有效;一旦脚本执行遇到go
,这些对象的作用域即被终止,后续批次将无法访问 - 注意与分号的区别:
;
仅作「同批次语句」分隔符,go
则是「批处理」分界符
- 🚨 存储过程、函数、触发器、视图等对象的定义语句,必须放在独立的 Batch 中,即需出现在上一个
raiserror
之后通常用空格分隔参数,但其实也允许直接写括号,例如:
1-- 这两种写法都合法
2raiserror ('错误', 16, 1); -- 👍 推荐
3raiserror('错误', 16, 1);
- 空格不是为了语法上的区分函数调用,而是 SQL 语言风格使然——关键字后面常用空格,函数调用则紧连括号
- 👍 推荐按官方习惯在
raiserror
后面加空格,可读性高,也符合行业标准
SQL Server 中
raiserror
的自定义错误抛出,规范参数为 Severity 16(常规用户级错误)、State 1(默认状态码),已成为业界通用做法。
⚠️ 注意:raiserror ('错误', 16, 1)
后面的代码确实还会继续执行,这是 T-SQL 的标准行为:除非是 严重级别 ≥ 20
,否则 raiserror
只是抛出一个错误消息,不会自动终止当前批处理或存储过程,需要手动使用 return
或 throw
来中断执行。
- Severity
11~19
:只会把错误消息抛出到客户端,如果在try...catch
内,会跳转到catch
,否则继续执行后续语句 - Severity
20
及以上:会断开当前连接,彻底终止执行- 但开发中不会使用这么高的严重级别,因为这会导致连接被强制断开,用户体验极差
- 不自动
return
:只有手动写上return
,或者在try
块里用raiserror
才会跳去catch
,否则raiserror
后的代码依然执行
1create table #WXJ (C1 varchar(500));
2
3declare @dividend int = 10, @divisor int = 0;
4
5if @divisor = 0
6begin
7 -- 👍 推荐自定义错误的参数组合:Severity=16,State=1
8 raiserror ('分母不能为 0', 16, 1);
9 print '这行及后续代码还是会被执行, 只是 SSMS 或 IntelliJ IDEA Database Plugin 在报错后会直接展示错误消息并阻断结果输出';
10 insert into #WXJ values ('想象中不被执行, 但实际确实被执行的代码行');
11 return; -- ⚠️ 一定要使用 return 语句来终止执行
12end;
13
14insert into #WXJ values ('这行及后续代码就不会被执行了, 感谢 return 语句的存在');
15
16-- 检查结果符合预期:仅有一行数据
17select * from #WXJ;
18
19drop table #WXJ;
用 THROW 抛出自定义错误
在 SQL Server(2012 及以上版本)中,推荐以 throw
语句取代传统的 raiserror
进行异常处理,throw
提供更一致的语法体验与错误捕获行为,尤其在 try...catch
结构内表现更为规范。
throw
的两种用法:
- 不带参数:在
catch
块内直接throw
,用于重新抛出当前捕获的异常,保留原始错误信息、编号、严重级别与状态码 - 带参数:直接抛出自定义错误消息,语法简洁,如
throw 50001, '自定义错误', 1;
在 SQL Server 中,自定义异常抛出建议使用
throw
,错误编号采用 50000 以上,状态码默认填 1,已成为主流通用做法。
1create table #WXJ (C1 varchar(500));
2
3declare @dividend int = 10, @divisor int = 0;
4
5if @divisor = 0
6begin
7 -- 👍 推荐自定义错误的参数组合:ErrorNumber={错误编号 > 50000},State=1
8 throw 50001, '分母不能为 0', 1;
9 print 'throw 抛错即终止, 无需 return';
10 insert into #WXJ values ('符合不被执行的预期');
11end;
12
13insert into #WXJ values ('这行及后续也都不会被执行, 感谢 throw 错误的特性');
14
15-- 检查结果符合预期:没有数据
16select * from #WXJ;
17
18drop table #WXJ;
在 try...catch
结构中 raiserror
vs throw
1-- raiserror + try...catch 示例
2begin try
3 -- 强制触发一个错误:除以零
4 select 1 / 0;
5end try
6begin catch
7 declare @err_msg nvarchar(4000) = error_message(),
8 @err_severity int = error_severity(),
9 @err_state int = error_state();
10
11 -- 用 raiserror 转抛异常,让上层或客户端接收到一致的错误格式
12 raiserror (@err_msg, @err_severity, @err_state);
13end catch;
1-- throw + try...catch 示例(👍 推荐)
2begin try
3 -- 强制触发一个错误:除以零
4 select 1 / 0;
5end try
6begin catch
7 -- 👍 推荐直接 throw 保留所有原始错误信息
8 throw;
9end catch;
THROW 与 RAISERROR 的最大不同
throw
与raiserror
最大不同在于throw
会直接中断后续 T-SQL 语句的执行,无需再写return
。
核心区别:
throw
:执行到throw
后,当前批处理直接终止,后续语句都不会再被执行,也不需要return
raiserror
:除非severity ≥ 20
,否则只是抛出错误消息,后面语句依然会执行(除非主动加return
或在try...catch
中跳转)
throw
用法:
1-- throw 终止流程
2throw 50001, '自定义错误', 1;
3print '这一行永远不会被执行';
raiserror
示例对比:
1-- raiserror 只是抛出错误消息,后续语句仍会执行
2raiserror ('这只是报错', 16, 1)
3print '这一行会被执行, 除非加 return'
存储过程最佳实践
注意:这里使用
varchar
类型,并配合排序规则(Collation)为Chinese_PRC_CI_AS
(即数据库默认采用简体中文(不区分大小写、区分重音),底层采用 GBK/936 简体中文代码页),故可覆盖简体中文和英文的日常存储和检索,不会有乱码问题。
- 仅限「简体中文+英文」场景,
varchar + Chinese_PRC_CI_AS
是合适且高效的选择(尤其在需要存储大量英文时,varchar
比nvarchar
更节省空间)
varchar(n)
限制的是字节数:存储英文比存储中文更节省空间,因为英文占 1 个字节,而中文占 2 个字节nvarchar(n)
限制的是字符数(与字节无关):每个字符(无论中英文)都占 2 个字节- 若存在更高字符集要求(比如涉及繁体中文、日韩字符、Emoji、各类特殊符号或未来扩展到其他语种),建议一开始就用
nvarchar
,以免后续迁移麻烦
1-- 删除旧表(如果已存在)
2if object_id('dbo.WXJ_TEST') is not null
3 drop table dbo.WXJ_TEST;
4
5-- 创建表
6create table dbo.WXJ_TEST (
7 test_id int identity primary key,
8 -- 在 SQL Server 中,如果某列被 unique 约束修饰,则只允许一个 null,插入两个及以上 null 会违反唯一约束!
9 test_name varchar(50) constraint WXJ_TEST_test_name_uq unique,
10);
11
12-- 1️⃣ 先插入一个 null 测试数据
13insert into dbo.WXJ_TEST (test_name) values (null);
14go -- 这里的 go 不是必须的
15
16
17-- 删除旧存储过程(如果已存在)
18if object_id('dbo.usp_wxj_test') is not null
19 drop procedure dbo.usp_wxj_test;
20go -- ⚠️ 这里是必须要有 go,因为存储过程的定义必须是批次中的第一个语句!
21
22
23-- 创建存储过程
24create procedure dbo.usp_wxj_test
25 @test_id int
26as
27begin
28 -- 👍 存储过程、触发器开头加 `set nocount on;` 属于业界标准的最佳实践
29 set nocount on;
30
31 begin try
32 -- 开始事务
33 -- 👍 推荐事务块缩进(嵌套事务要层层缩进),与一般代码块一致,以提升可读性和可维护性
34 begin transaction;
35
36 -- 正常插入新数据
37 insert into dbo.WXJ_TEST (test_name) values (newid());
38 print '插入 UUID 成功';
39
40 -- 参数校验(业务层校验)
41 if @test_id is null or @test_id <= 0
42 throw 50001, '参数 @test_id 必须为正整数且不可为 NULL', 1;
43
44 print '参数 @test_id = ' + cast(@test_id as varchar);
45
46 -- 2️⃣ 故意插入重复数据,触发唯一约束错误
47 if @test_id >= 10
48 insert into dbo.WXJ_TEST (test_name) values (null);
49
50 print '开始查询';
51 select * from dbo.WXJ_TEST where test_id = @test_id;
52
53 -- 提交事务
54 commit transaction;
55 end try
56 begin catch
57 -- 判断当前是否有活动事务,xact_state() 函数返回值:
58 -- 0:没有活动事务
59 -- 1:有活动事务(可以提交或回滚)
60 -- -1:有活动事务,但已处于错误状态(只能回滚,不能提交)
61 -- 🤔 本测试代码异常时,此处的事务状态为 1
62 print '异常后事务状态: ' + cast(xact_state() as varchar);
63
64 -- 如果有活动事务,则回滚事务
65 if xact_state() <> 0
66 begin
67 print '虽然有些错误不会破坏事务的一致性(即 xact_state() = 1),故此时可安全提交,但大部分需求还是选择回滚';
68 rollback transaction;
69 end; -- ⚠️ 这个分号是必须的!因为后面紧接着是 throw 语句
70
71 -- ⚠️ 注意:throw 必须以分号结尾,且前一语句也要用分号结束
72 throw;
73 end catch;
74end;
75-- ⚠️ 必须要有 go,否则 SQL Server 会将后续的 `exec dbo.usp_wxj_test ` 语句视为存储过程定义的一部分,结果存储过程会递归调用自身,最终因超出最大嵌套深度报错,这里则是因违反了唯一约束而报错。
76go
77
78
79-- 正常执行
80exec dbo.usp_wxj_test @test_id = 1;
81
82-- ❌ 此语句会报错 [违反了 UNIQUE KEY 约束“uq_test_name”。不能在对象“dbo.WXJ_TEST”中插入重复键。重复键值为 (<NULL>)。]
83exec dbo.usp_wxj_test @test_id = 10;
84
85print '因为上一条语句报错, 下面的语句不会执行!';
86select * from dbo.WXJ_TEST;
事务还原点(Savepoint)
事务还原点允许在一次事务内「局部回退」而无需全撤销。适合处理需多步提交、局部容错的复杂业务场景。比如大批量处理(插入/更新)时,部分失败无需全撤销。
1-- 删除旧表(如果已存在)
2if object_id('dbo.WXJ_TEST') is not null
3 drop table dbo.WXJ_TEST;
4
5-- 创建表
6create table dbo.WXJ_TEST (
7 test_id int identity primary key,
8 test_name varchar(50) constraint uq_test_name unique,
9);
10
11-- 插入一个 null 测试数据
12insert into dbo.WXJ_TEST (test_name) values (null);
13go
14
15
16-- 删除旧存储过程(如果已存在)
17if object_id('dbo.usp_wxj_test') is not null
18 drop procedure dbo.usp_wxj_test;
19go
20
21
22-- 创建存储过程
23create procedure dbo.usp_wxj_test
24as
25begin
26 set nocount on;
27
28 begin try
29 begin transaction;
30
31 print '1. 插入第一条数据, 设置还原点';
32 insert into dbo.WXJ_TEST (test_name) values ('第一次');
33 save transaction tx_first; -- 设置还原点
34
35 print '2. 插入第二条数据, 设置还原点';
36 insert into dbo.WXJ_TEST (test_name) values ('第二次');
37 save transaction tx_second; -- 设置还原点
38
39 print '3. 插入第三条数据, 但没有设置还原点';
40 insert into dbo.WXJ_TEST (test_name) values ('第三次');
41
42 -- 此时发现有业务异常,回滚到 tx_first,即撤销 tx_first 之后的所有操作
43 rollback transaction tx_first;
44
45 commit transaction;
46 end try
47 begin catch
48 if xact_state() <> 0
49 begin
50 print '出错即回滚';
51 rollback transaction;
52 end;
53
54 throw;
55 end catch;
56end;
57go
58
59
60exec usp_wxj_test;
61
62-- 确认测试表数据
63-- 1|<null>
64-- 2|第一次
65select * from dbo.WXJ_TEST;