关于tsql:如何在T-SQL存储过程中使用可选参数?

How can I use optional parameters in a T-SQL stored procedure?

我正在创建一个存储过程来搜索表。我有许多不同的搜索字段,它们都是可选的。是否有方法创建将处理此问题的存储过程?假设我有一个表,有四个字段:id、firstname、lastname和title。我可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE PROCEDURE spDoSearch
    @FirstName varchar(25) = null,
    @LastName varchar(25) = null,
    @Title varchar(25) = null
AS
    BEGIN
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
            FirstName = ISNULL(@FirstName, FirstName) AND
            LastName = ISNULL(@LastName, LastName) AND
            Title = ISNULL(@Title, Title)
    END

这类作品。但是,它忽略firstname、lastname或title为空的记录。如果搜索参数中未指定标题,我希望包括标题为空的记录-firstname和lastname的标题相同。我知道我可以用动态SQL来实现这一点,但我想避免这种情况。


基于给定参数动态更改搜索是一个复杂的主题,以一种方式对另一种方式进行搜索,即使只有很小的差异,也可能会产生巨大的性能影响。关键是要使用索引,忽略压缩代码,忽略担心重复代码,必须制定一个好的查询执行计划(使用索引)。

阅读本文并考虑所有方法。您的最佳方法将取决于您的参数、数据、模式和实际使用情况:

Erland Sommarskog在T-SQL中的动态搜索条件

动态SQL的诅咒与祝福

如果您有正确的SQL Server 2008版本(SQL 2008 SP1 CU5(10.0.2746)及更高版本),则可以使用以下小技巧实际使用索引:

OPTION (RECOMPILE)添加到查询中,请参阅erland的文章,在根据本地变量的运行时值创建查询计划之前,SQL Server将从(@LastName IS NULL OR LastName= @LastName)内解析OR并使用索引。

这将适用于任何SQL Server版本(返回正确的结果),但只有在使用SQL 2008 SP1 CU5(10.0.2746)和更高版本时才包括选项(重新编译)。选项(重新编译)将重新编译您的查询,只有列出的版本将根据本地变量的当前运行时值重新编译它,这将为您提供最佳性能。如果不在该版本的SQL Server 2008上,只需关闭该行即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE PROCEDURE spDoSearch
    @FirstName varchar(25) = null,
    @LastName varchar(25) = null,
    @Title varchar(25) = null
AS
    BEGIN
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
                (@FirstName IS NULL OR (FirstName = @FirstName))
            AND (@LastName  IS NULL OR (LastName  = @LastName ))
            AND (@Title     IS NULL OR (Title     = @Title    ))
        OPTION (RECOMPILE) ---<<<<use if on for SQL 2008 SP1 CU5 (10.0.2746) and later
    END


@km的回答是很好的,但没有完全跟进他早期的建议之一;

..., ignore compact code, ignore worrying about repeating code, ...

如果您希望获得最佳性能,那么应该为可选条件的每个可能组合编写一个定制查询。这听起来可能很极端,如果您有很多可选的标准,那么它可能是,但是性能通常是工作和结果之间的权衡。在实践中,可能会有一组通用的参数组合,这些参数组合可以以定制查询为目标,然后是所有其他组合的通用查询(根据其他答案)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
CREATE PROCEDURE spDoSearch
    @FirstName varchar(25) = null,
    @LastName varchar(25) = null,
    @Title varchar(25) = null
AS
BEGIN

    IF (@FirstName IS NOT NULL AND @LastName IS NULL AND @Title IS NULL)
        -- Search by first name only
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
            FirstName = @FirstName

    ELSE IF (@FirstName IS NULL AND @LastName IS NOT NULL AND @Title IS NULL)
        -- Search by last name only
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
            LastName = @LastName

    ELSE IF (@FirstName IS NULL AND @LastName IS NULL AND @Title IS NOT NULL)
        -- Search by title only
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
            Title = @Title

    ELSE IF (@FirstName IS NOT NULL AND @LastName IS NOT NULL AND @Title IS NULL)
        -- Search by first and last name
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
            FirstName = @FirstName
            AND LastName = @LastName

    ELSE
        -- Search by any other combination
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
                (@FirstName IS NULL OR (FirstName = @FirstName))
            AND (@LastName  IS NULL OR (LastName  = @LastName ))
            AND (@Title     IS NULL OR (Title     = @Title    ))

END

这种方法的优点是,在由定制查询处理的常见情况下,查询的效率尽可能高——不受不适用条件的影响。此外,索引和其他性能增强可以针对特定的定制查询,而不是试图满足所有可能的情况。


在以下情况下可以这样做,

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE PROCEDURE spDoSearch
   @FirstName varchar(25) = null,
   @LastName varchar(25) = null,
   @Title varchar(25) = null
AS
  BEGIN
      SELECT ID, FirstName, LastName, Title
      FROM tblUsers
      WHERE
        (@FirstName IS NULL OR FirstName = @FirstName) AND
        (@LastNameName IS NULL OR LastName = @LastName) AND
        (@Title IS NULL OR Title = @Title)
END

但是依赖于数据有时更好地创建动态查询并执行它们。


延长你的WHERE条件:

1
2
3
4
5
6
7
WHERE
    (FirstName = ISNULL(@FirstName, FirstName)
    OR COALESCE(@FirstName, FirstName, '') = '')
AND (LastName = ISNULL(@LastName, LastName)
    OR COALESCE(@LastName, LastName, '') = '')
AND (Title = ISNULL(@Title, Title)
    OR COALESCE(@Title, Title, '') = '')

也就是说,将不同的情况与布尔条件结合起来。


晚会迟到了五年。

在已接受答案的提供链接中提到了这个问题,但是我认为它应该得到一个明确的答案,因为它基于所提供的参数动态地构建查询。例如。:

安装程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- drop table Person
create table Person
(
    PersonId INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_Person PRIMARY KEY,
    FirstName NVARCHAR(64) NOT NULL,
    LastName NVARCHAR(64) NOT NULL,
    Title NVARCHAR(64) NULL
)
GO

INSERT INTO Person (FirstName, LastName, Title)
VALUES ('Dick', 'Ormsby', 'Mr'), ('Serena', 'Kroeger', 'Ms'),
    ('Marina', 'Losoya', 'Mrs'), ('Shakita', 'Grate', 'Ms'),
    ('Bethann', 'Zellner', 'Ms'), ('Dexter', 'Shaw', 'Mr'),
    ('Zona', 'Halligan', 'Ms'), ('Fiona', 'Cassity', 'Ms'),
    ('Sherron', 'Janowski', 'Ms'), ('Melinda', 'Cormier', 'Ms')
GO

程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ALTER PROCEDURE spDoSearch
    @FirstName varchar(64) = null,
    @LastName varchar(64) = null,
    @Title varchar(64) = null,
    @TopCount INT = 100
AS
BEGIN
    DECLARE @SQL NVARCHAR(4000) = '
        SELECT TOP '
+ CAST(@TopCount AS VARCHAR) + ' *
        FROM Person
        WHERE 1 = 1'


    PRINT @SQL

    IF (@FirstName IS NOT NULL) SET @SQL = @SQL + ' AND FirstName = @FirstName'
    IF (@LastName IS NOT NULL) SET @SQL = @SQL + ' AND FirstName = @LastName'
    IF (@Title IS NOT NULL) SET @SQL = @SQL + ' AND Title = @Title'

    EXEC sp_executesql @SQL, N'@TopCount INT, @FirstName varchar(25), @LastName varchar(25), @Title varchar(64)',
         @TopCount, @FirstName, @LastName, @Title
END
GO

用法

1
2
exec spDoSearch @TopCount = 3
exec spDoSearch @FirstName = 'Dick'

赞成的意见:

  • 易于书写和理解
  • 灵活性-很容易生成有关细流过滤的查询(例如动态顶部)

欺骗:

  • 可能的性能问题取决于提供的参数、索引和数据量

不是直接的答案,而是与问题有关,也就是说,大问题。

通常,这些筛选存储过程不会四处浮动,而是从某个服务层调用。这就留下了将业务逻辑(过滤)从SQL移到服务层的选项。

一个例子是使用linq2sql根据提供的过滤器生成查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    public IList<SomeServiceModel> GetServiceModels(CustomFilter filters)
    {
        var query = DataAccess.SomeRepository.AllNoTracking;

        // partial and insensitive search
        if (!string.IsNullOrWhiteSpace(filters.SomeName))
            query = query.Where(item => item.SomeName.IndexOf(filters.SomeName, StringComparison.OrdinalIgnoreCase) != -1);
        // filter by multiple selection
        if ((filters.CreatedByList?.Count ?? 0) > 0)
            query = query.Where(item => filters.CreatedByList.Contains(item.CreatedById));
        if (filters.EnabledOnly)
            query = query.Where(item => item.IsEnabled);

        var modelList = query.ToList();
        var serviceModelList = MappingService.MapEx<SomeDataModel, SomeServiceModel>(modelList);
        return serviceModelList;
    }

赞成的意见:

  • 基于提供的过滤器动态生成的查询。不需要参数嗅探或重新编译提示
  • 为那些在OOP世界里的人写东西比较容易
  • 通常性能友好,因为将发出"简单"查询(但仍然需要适当的索引)

欺骗:

  • 可能会达到linq2ql限制,并根据情况强制降级到linq2objects或返回纯SQL解决方案
  • 不小心编写LINQ可能会生成糟糕的查询(如果加载了导航属性,则可能会生成许多查询)

这也适用于:

1
2
3
4
5
    ...
    WHERE
        (FirstName IS NULL OR FirstName = ISNULL(@FirstName, FirstName)) AND
        (LastName IS NULL OR LastName = ISNULL(@LastName, LastName)) AND
        (Title IS NULL OR Title = ISNULL(@Title, Title))