校验必须放在INSERT之前,否则事务已部分生效;所有业务规则检查须在写入前完成;优先用THROW报错并确保事务回滚;OUTPUT参数用于返回业务数据,RETURN仅表示状态;事务需包裹校验与写入全过程。

校验必须放在INSERT之前,否则事务已部分生效
SQL Server 存储过程不会自动拦截非法数据,INSERT 一旦执行,就可能写入脏数据——哪怕后面跟着 RAISERROR 或 THROW。常见错误是把 IF 校验写在 INSERT 后面,结果主键冲突或长度超限报错时,事务已无法干净回滚。
正确做法:所有业务规则检查(如非空、长度、数值范围、外键存在性、余额是否充足)都必须在任何 INSERT 或 UPDATE 语句之前完成。
- 用
IF NOT EXISTS (SELECT 1 FROM ... WHERE ...)检查外键依赖,比LEFT JOIN+IS NULL更直观且易调试 - 字符串长度用
LEN(@param) > 50,别用DATALENGTH——后者含尾随空格,语义不符 - 数值范围检查直接写
IF @amount 1000000,避免嵌套CASE增加阅读负担 - 涉及多条件组合时,优先用
AND/OR显式连接,别依赖运算符优先级隐式推导
用THROW中断执行,别用PRINT或RETURN
PRINT 只输出消息,不终止后续语句;RETURN 会跳出当前过程,但上层调用者收不到错误状态码,应用层可能误判为成功。SQL Server 2012+ 推荐统一用 THROW 主动报错。
示例:THROW 50000, '订单金额超出单笔限额', 1 会立刻中止批处理,触发客户端异常捕获,并保证事务自动回滚(前提是已开启显式事务)。
- 错误号建议用 50000–59999 范围,避免与系统错误冲突
- 第三个参数(状态值)固定填
1即可,无需动态计算 - 不要在循环体内对每行都
THROW——应先用SELECT COUNT(*)或EXISTS做集合级预检,再统一处理 - 若需返回具体字段名,拼接消息时用
CONCAT('字段 ', @field_name, ' 不合法'),避免+连接 NULL 导致整条消息变 NULL
输出参数和返回值要分清用途
存储过程的 RETURN 值只能是整数,仅适合传递简单状态(如 0=成功,1=参数错误,2=业务拒绝)。真正需要返回的数据(如生成的 @OrderID、校验后的 @FinalAmount)必须用 OUTPUT 参数。
注意:OUTPUT 参数的值只在过程退出后才传回调用方,且必须在调用时显式声明 OUTPUT 关键字,否则值不会回写。
-
RETURN不可用于返回业务数据,它本质是过程执行状态码 - 多个输出值优先用
OUTPUT参数,而非靠SELECT返回结果集——后者在某些客户端(如 ODBC)中需额外释放行集才能拿到OUTPUT值 - 输出参数类型要与实际赋值严格一致,比如
@id INT OUTPUT就不能赋'123'字符串,否则隐式转换失败 - 如果过程可能被嵌套调用,避免重用同名
OUTPUT参数变量,防止作用域混淆
事务边界要包裹整个校验+写入流程
校验通过后到 INSERT 完成前,存在时间窗口——并发请求可能修改依赖数据(如库存、余额)。必须用 BEGIN TRY / BEGIN TRANSACTION 包裹从校验到提交的全部逻辑,且在 CATCH 块中显式 ROLLBACK。
别依赖默认自动提交:即使没写 BEGIN TRAN,单条 INSERT 也是自动事务,但校验和写入不在同一事务内,就失去原子性保障。
- 校验中若需查其他表(如用户余额),建议加
WITH (UPDLOCK, HOLDLOCK)防止并发修改,尤其在高并发下单据类场景 -
SET XACT_ABORT ON应放在过程开头,确保运行时错误(如死锁)也能触发回滚 - 避免在事务中调用含
COMMIT的嵌套过程,会导致“不可提交的事务”错误 - 临时表操作(如
#temp)不受事务控制,但其内容在事务回滚后自然消失,无需额外清理

















