个人技术站点JASONWU

Keep Coding


  • 主页

  • 文章

  • 搜索

SQL Server 异常处理:自定义错误、事务管理

新建: 2025-08-06 编辑: 2025-08-09   |   分类: 后端开发   | 字数: 3854 字

用 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
  • go(批处理分隔符):SQL Server 相关工具的批处理分隔符,不属于 SQL 语言本身,也不是数据库引擎识别的语法或语句,常用于分隔无法在同一 Batch 中执行的 SQL
    • 🚨 存储过程、函数、触发器、视图等对象的定义语句,必须放在独立的 Batch 中,即需出现在上一个 go 之后(或脚本开头),并以 go 结束(或脚本结尾)
    • 批处理级作用域:所有在 go 之前声明的局部变量、临时表或上下文信息,仅在当前批次有效;一旦脚本执行遇到 go,这些对象的作用域即被终止,后续批次将无法访问
    • 注意与分号的区别:; 仅作「同批次语句」分隔符,go 则是「批处理」分界符

raiserror 之后通常用空格分隔参数,但其实也允许直接写括号,例如:

SQL
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 后的代码依然执行
SQL
 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,已成为主流通用做法。

SQL
 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

SQL
 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;
SQL
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 用法:

SQL
1-- throw 终止流程
2throw 50001, '自定义错误', 1;
3print '这一行永远不会被执行';

raiserror 示例对比:

SQL
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,以免后续迁移麻烦
SQL
 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)

事务还原点允许在一次事务内「局部回退」而无需全撤销。适合处理需多步提交、局部容错的复杂业务场景。比如大批量处理(插入/更新)时,部分失败无需全撤销。

SQL
 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;
#SQL# #事务管理# #SQL Server#

文章:SQL Server 异常处理:自定义错误、事务管理

链接:https://www.wuxianjie.net/posts/backend/mssql-error-handling/

作者:吴仙杰

文章: 本博客文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议,转载请注明出处!

SQL Server 字段级恢复:基于主键的备份表关联方法
SQL Server 变量声明与赋值
  • 文章目录
  • 站点概览
吴仙杰

吴仙杰

🔍 Ctrl+K / ⌘K

27 文章
9 分类
25 标签
邮箱 GitHub
  • 用 RAISERROR 抛出自定义错误
  • 用 THROW 抛出自定义错误
  • THROW 与 RAISERROR 的最大不同
  • 存储过程最佳实践
    • 事务还原点(Savepoint)
© 2021-2025 吴仙杰 保留所有权利 All Rights Reserved
浙公网安备 33010302003726号 浙ICP备2021017187号-1
0%