在SQLServer的实际开发场景中,很多业务查询需要根据用户选择的不同条件动态拼接查询参数,如果使用固定参数的存储过程,往往需要编写多个重载版本的存储过程,维护成本较高。实现动态参数调用存储过程可以很好地解决这个问题,下面介绍两种常用的实现方式。

方法一:使用动态SQL拼接实现
这种方式的核心思路是在存储过程内部根据传入的参数是否为空,动态拼接SQL语句,最后执行拼接好的动态SQL。首先创建示例表用于测试:
-- 创建测试表
CREATE TABLE TestUser (
Id INT IDENTITY(1,1) PRIMARY KEY,
UserName NVARCHAR(50),
Age INT,
City NVARCHAR(50)
)
GO
-- 插入测试数据
INSERT INTO TestUser (UserName, Age, City) VALUES
('张三', 25, '北京'),
('李四', 30, '上海'),
('王五', 25, '广州'),
('赵六', 35, '北京')
GO
接下来创建支持动态参数的存储过程,参数可以为空,为空时不作为查询条件:
CREATE PROCEDURE QueryUserByDynamicParam
@UserName NVARCHAR(50) = NULL, -- 用户名参数,可为空
@Age INT = NULL, -- 年龄参数,可为空
@City NVARCHAR(50) = NULL -- 城市参数,可为空
AS
BEGIN
SET NOCOUNT ON;
-- 定义动态SQL变量
DECLARE @Sql NVARCHAR(MAX)
DECLARE @WhereSql NVARCHAR(MAX) = ''
-- 拼接基础查询语句
SET @Sql = 'SELECT Id, UserName, Age, City FROM TestUser'
-- 根据参数是否为空拼接条件
IF @UserName IS NOT NULL
SET @WhereSql = @WhereSql + ' AND UserName = @InnerUserName'
IF @Age IS NOT NULL
SET @WhereSql = @WhereSql + ' AND Age = @InnerAge'
IF @City IS NOT NULL
SET @WhereSql = @WhereSql + ' AND City = @InnerCity'
-- 如果有条件拼接WHERE子句
IF LEN(@WhereSql) > 0
BEGIN
-- 去掉第一个AND,添加WHERE关键字
SET @WhereSql = ' WHERE ' + STUFF(@WhereSql, 1, 4, '')
SET @Sql = @Sql + @WhereSql
END
-- 定义参数列表,避免SQL注入
DECLARE @ParamDefinition NVARCHAR(MAX)
SET @ParamDefinition = '@InnerUserName NVARCHAR(50), @InnerAge INT, @InnerCity NVARCHAR(50)'
-- 执行动态SQL
EXEC sp_executesql @Sql, @ParamDefinition, @InnerUserName = @UserName, @InnerAge = @Age, @InnerCity = @City
END
GO
调用该存储过程的示例:
-- 只传入用户名参数 EXEC QueryUserByDynamicParam @UserName = '张三' -- 传入年龄和城市参数 EXEC QueryUserByDynamicParam @Age = 25, @City = '北京' -- 传入所有参数 EXEC QueryUserByDynamicParam @UserName = '李四', @Age = 30, @City = '上海' -- 不传入任何参数,查询所有数据 EXEC QueryUserByDynamicParam
方法二:使用OPTION (RECOMPILE)实现
这种方式不需要拼接动态SQL,直接编写固定结构的查询语句,通过OPTION (RECOMPILE)提示让SQLServer在每次执行时重新编译执行计划,自动忽略值为NULL的参数条件。创建对应的存储过程:
CREATE PROCEDURE QueryUserByRecompile
@UserName NVARCHAR(50) = NULL,
@Age INT = NULL,
@City NVARCHAR(50) = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT Id, UserName, Age, City
FROM TestUser
WHERE (@UserName IS NULL OR UserName = @UserName)
AND (@Age IS NULL OR Age = @Age)
AND (@City IS NULL OR City = @City)
OPTION (RECOMPILE) -- 每次执行重新编译执行计划
END
GO
该存储过程的调用方式和第一种方法完全一致,同样支持任意组合参数传入:
-- 只传入城市参数 EXEC QueryUserByRecompile @City = '北京' -- 传入年龄参数 EXEC QueryUserByRecompile @Age = 25
两种方法的对比
两种方式各有适用场景,具体差异如下:
| 对比项 | 动态SQL拼接方式 | OPTION (RECOMPILE)方式 |
|---|---|---|
| 实现复杂度 | 较高,需要拼接SQL和处理参数 | 较低,直接编写条件即可 |
| 执行计划缓存 | 可以缓存不同参数组合的执行计划 | 每次执行重新编译,不缓存执行计划 |
| 适用场景 | 参数组合较多且查询频繁的场景 | 参数组合少、执行频率低的场景 |
| SQL注入风险 | 使用sp_executesql参数化则无风险 | 无风险 |
注意事项
- 使用动态SQL拼接时,一定要通过
sp_executesql的参数化方式传参,不要直接拼接字符串,避免SQL注入漏洞。 - 如果存储过程的查询逻辑复杂,使用
OPTION (RECOMPILE)可能会导致每次编译的开销过高,影响性能。 - 动态参数存储过程的参数建议设置默认值NULL,避免调用时因为缺少参数导致执行报错。