SQL Server存储过程中业务规则校验应前置在CREATE PROCEDURE内,用IF+THROW按约束类、业务类、计算类顺序检查;优先用CHECK CONSTRAINT固化简单规则,共用逻辑封装为内联表值函数(ITVF),避免标量函数和通用校验过程。

SQL Server 存储过程中怎么写业务规则校验?
直接在 CREATE PROCEDURE 里用 IF + RAISERROR 或 THROW 做前置检查,是最常用也最可控的方式。别指望靠外层应用兜底——等数据进到存储过程,校验必须当场完成。
常见错误是把校验逻辑写成独立函数再反复调用,结果发现执行计划里多了大量标量 UDF 调用开销,性能掉一截;或者用 TRY...CATCH 包整个逻辑,反而掩盖了本该提前拦截的业务违规(比如金额为负、状态非法)。
- 校验顺序很重要:先查约束类(如主键/外键是否存在),再查业务类(如“订单不能重复提交”),最后查计算类(如“折扣后价格不能低于成本价”)
- 用
EXISTS替代SELECT COUNT(*)做存在性判断,避免全表扫描 - 对字符串长度、数值范围等简单规则,优先用
CHECK CONSTRAINT在表级固化,存储过程里只管复合逻辑
如何集中管理多处存储过程共用的校验逻辑?
SQL Server 没有“校验模块”这种东西,所谓集中管理,本质是复用可预测、可测试的 SQL 片段。最稳妥的是用内联表值函数(ITVF),不是标量函数(SVF),更不是视图。
典型陷阱是把校验封装成 SELECT dbo.fn_check_order_status(@order_id) 这种标量函数,它会在每一行上执行一次,遇到批量插入/更新就卡死;而 SELECT * FROM dbo.tvf_validate_order(@order_id) 是内联的,优化器能把它展开进执行计划,不额外增加开销。
- 所有 ITVF 必须是单个
SELECT,不能含变量赋值、IF或循环 - 参数尽量少且类型明确,避免
sql_variant或过度泛型 - 返回结果固定两列:
is_valid BIT和error_message NVARCHAR(256),方便各存储过程统一处理
校验失败时怎么返回清晰错误给调用方?
用 THROW(SQL Server 2012+)比 RAISERROR 更可靠:它会中止当前批处理、保留原始错误号和状态,并触发客户端的异常捕获机制。别用 PRINT 或返回码(RETURN 1)代替错误抛出——应用层根本收不到。
容易踩的坑是硬编码错误号,比如 THROW 50001, '订单已关闭', 1,结果上线后和其他模块冲突;或者漏写状态参数,导致重试时无法区分是临时失败还是永久拒绝。
- 自定义错误号从
50000起始,用sp_addmessage预注册描述,确保ERROR_MESSAGE()返回中文 - 错误消息里带关键上下文,比如
'订单 ' + CAST(@order_id AS VARCHAR(20)) + ' 状态非法:当前为「已取消」,不允许再次发货' - 不要在事务中校验失败后还继续执行后续语句——
THROW后自动退出,但得确认没被外层TRY...CATCH吞掉
为什么不能把所有校验都塞进一个“通用校验存储过程”?
因为校验依赖上下文:同一张表的字段,在插入时要非空,在更新时可能允许为空;某个状态转移是否合法,取决于当前值和目标值的组合。硬写一个 usp_validate(@table_name, @action, @json_data),最终只会变成难以调试、无法索引、执行计划混乱的黑盒。
真实项目里见过最麻烦的情况,是用动态 SQL 拼接校验条件,结果参数化失败、注入风险高、执行计划缓存失效——看起来“集中”,实则失控。
- 按业务动作建模,不是按表建模:比如
usp_create_invoice和usp_approve_invoice各自有校验逻辑,哪怕都操作invoice表 - 共用逻辑抽成 ITVF,但调用方决定何时、如何用,而不是由一个“总控过程”调度
- 复杂校验(如工作流状态机)建议用查找表 + 关系约束,而不是纯代码判断
真正难的从来不是写几条 IF EXISTS,而是厘清每个业务动作下,“什么算错”以及“错到什么程度必须拦住”。这点想模糊,后面补救成本远高于初期多花十分钟画状态转换图。










