在企业级数据库开发中,存储过程作为业务逻辑的重要载体,其规范性和可维护性直接影响系统的稳定性和团队协作效率。我们公司经过多年实践,总结出一套完整的SQL Server存储过程开发规范,主要适用于SQL Server 2012及以上版本的核心业务场景。
这套规范的核心价值体现在三个方面:
实际案例:在订单系统中,采用本规范后,存储过程导致的线上问题减少了78%,新成员上手时间缩短了60%。
根据单一职责原则,我们将存储过程分为三类,每类有明确的职责边界:
| 类型 | 职责 | 结果返回 | 事务使用 | 典型场景 |
|---|---|---|---|---|
| 校验型 | 参数/状态/权限校验 | 无(失败抛异常) | 禁止使用 | 数据存在性检查 |
| 业务型 | 核心业务处理 | 无(成功无返回) | 必须使用 | 订单创建/更新 |
| 查询型 | 数据检索 | 结果集(ResultSet) | 视情况使用 | 报表查询 |
我们采用"成功走流程,失败走异常"的原则:
SELECT code,msg和异常抛出sql复制-- 错误示例(禁止)
IF @error_condition = 1
BEGIN
SELECT 50001 AS code, '错误消息' AS msg;
RETURN;
END
-- 正确示例(规范)
IF @error_condition = 1
BEGIN
THROW 50001, N'错误消息', 1;
END
我们建立了完整的错误码分类体系,所有自定义错误码必须≥50000:
| 错误码区间 | 含义 | 典型场景 |
|---|---|---|
| 50000-50999 | 参数/数据校验错误 | 无效ID、格式错误 |
| 51000-51999 | 业务规则错误 | 库存不足、状态冲突 |
| 52000-52999 | 权限/租户错误 | 无访问权限 |
| 53000-53999 | 并发控制错误 | 乐观锁冲突 |
异常抛出必须使用标准格式:
sql复制THROW 50001, N'无效单据ID', 1;
-- 参数说明:错误码、消息、状态(固定为1)
采用sp_[模块]_[业务]_[动作]三段式结构:
sql复制-- 示例
sp_order_payment_create -- 订单支付创建
sp_user_role_assign -- 用户角色分配
所有参数必须使用@p_前缀:
sql复制CREATE PROCEDURE sp_example
@p_id BIGINT, -- 主键ID
@p_tenant_id INT, -- 租户ID
@p_operation_by NVARCHAR(50) -- 操作人
校验型存储过程专注于前置条件检查,典型模板如下:
sql复制CREATE PROCEDURE sp_check_order_exists
@p_order_id BIGINT,
@p_require_status VARCHAR(20) = NULL
AS
BEGIN
SET NOCOUNT ON;
-- 存在性检查
IF NOT EXISTS (
SELECT 1 FROM t_order
WHERE id = @p_order_id
)
BEGIN
THROW 50001, N'订单不存在', 1;
END
-- 可选状态检查
IF @p_require_status IS NOT NULL
BEGIN
IF NOT EXISTS (
SELECT 1 FROM t_order
WHERE id = @p_order_id
AND status = @p_require_status
)
BEGIN
THROW 50002, N'订单状态不符合要求', 1;
END
END
END
业务型存储过程必须使用以下事务模板:
sql复制CREATE PROCEDURE sp_order_confirm
@p_order_id BIGINT,
@p_operator NVARCHAR(50)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- 关键设置:运行时错误自动回滚
DECLARE @TranStarted BIT = 0;
BEGIN TRY
-- 事务开启规则
IF @@TRANCOUNT = 0
BEGIN
BEGIN TRAN;
SET @TranStarted = 1;
END
-- 1. 参数校验(调用校验型SP)
EXEC sp_check_order_exists @p_order_id, 'UNPAID';
-- 2. 业务逻辑
UPDATE t_order
SET status = 'CONFIRMED',
confirm_time = GETDATE(),
confirm_by = @p_operator
WHERE id = @p_order_id;
-- 3. 记录操作日志
INSERT INTO order_operation_log(...)
VALUES (...);
-- 提交规则:仅最外层事务提交
IF @TranStarted = 1
COMMIT TRAN;
END TRY
BEGIN CATCH
-- 回滚规则
IF @TranStarted = 1 AND XACT_STATE() <> 0
ROLLBACK TRAN;
-- 错误日志(不吞异常)
EXEC dbo.log_proc_error
@proc_name = 'sp_order_confirm',
@error_code = ERROR_NUMBER(),
@error_msg = ERROR_MESSAGE();
THROW; -- 重新抛出异常
END CATCH
END
@@TRANCOUNT判断是否需要开启新事务实测数据:正确使用XACT_ABORT可使事务相关错误减少90%以上
sql复制UPDATE t_inventory
SET stock = stock - @p_quantity,
update_time = GETDATE()
WHERE product_id = @p_product_id
AND stock >= @p_quantity; -- 原子性检查
IF @@ROWCOUNT = 0
BEGIN
THROW 53001, N'库存不足或商品不存在', 1;
END
sql复制UPDATE t_contract
SET content = @p_content,
version = version + 1
WHERE id = @p_contract_id
AND version = @p_version; -- 乐观锁控制
IF @@ROWCOUNT = 0
BEGIN
THROW 53002, N'数据已被修改,请刷新后重试', 1;
END
sql复制BEGIN TRAN;
-- 先锁定记录
SELECT 1
FROM t_account WITH (UPDLOCK, ROWLOCK)
WHERE user_id = @p_user_id;
-- 业务处理
UPDATE t_account
SET balance = balance - @p_amount
WHERE user_id = @p_user_id
AND balance >= @p_amount;
COMMIT TRAN;
sql复制-- 表设计
CREATE TABLE t_payment (
id BIGINT PRIMARY KEY,
payment_no VARCHAR(64) UNIQUE, -- 唯一业务编号
...
);
-- 存储过程实现
BEGIN TRY
INSERT INTO t_payment(payment_no, ...)
VALUES (@p_payment_no, ...);
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 2627 -- 唯一键冲突
BEGIN
THROW 51001, N'重复支付', 1;
END
THROW;
END CATCH
sql复制UPDATE t_order
SET status = 'SHIPPED',
ship_time = GETDATE()
WHERE id = @p_order_id
AND status = 'CONFIRMED'; -- 确保前置状态
IF @@ROWCOUNT = 0
BEGIN
-- 可能是重复调用或状态错误
THROW 51002, N'订单不满足发货条件', 1;
END
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 事务不生效 | 忘记设置XACT_ABORT ON | 确保模板开头有SET XACT_ABORT ON |
| 嵌套事务混乱 | 未正确检查@@TRANCOUNT | 严格使用标准事务模板 |
| 并发更新丢失 | 未使用乐观锁或行锁 | 添加版本号或使用UPDLOCK |
| 性能低下 | 校验SP未使用索引 | 检查WHERE条件中的索引使用 |
sql复制-- 使用局部变量避免参数嗅探
DECLARE @local_id BIGINT = @p_id;
SELECT ... WHERE id = @local_id;
sql复制-- 使用内存优化临时表
SELECT * INTO #temp FROM ... WHERE ...;
-- 处理完成后显式删除
DROP TABLE #temp;
sql复制-- 避免单条循环处理
INSERT INTO target_table(...)
SELECT ... FROM source_table
WHERE ...;
sql复制-- 关键查询强制使用索引
SELECT ... FROM table WITH (INDEX(ix_name))
WHERE ...;
架构审查:
代码审查:
性能审查:
命名约定:
code复制sp_模块_业务_动作_v版本号
示例:sp_order_payment_create_v2
变更日志:
sql复制/*
* 修改人:张三
* 修改时间:2023-08-20
* 变更内容:增加支付超时检查
* 版本号:v1.1
*/
灰度发布:
在实际项目中,我们通过SQL Server的扩展事件(XEvent)监控存储过程执行情况,定期生成性能报告并优化热点过程。同时建立代码模板库,新成员可以通过模板快速上手规范开发。