【问题标题】:How can I use optional parameters in a T-SQL stored procedure?如何在 T-SQL 存储过程中使用可选参数?
【发布时间】:2016-06-08 10:54:43
【问题描述】:

我正在创建一个存储过程来通过表进行搜索。我有许多不同的搜索字段,所有这些都是可选的。有没有办法创建一个存储过程来处理这个问题?假设我有一个包含四个字段的表:ID、FirstName、LastName 和 Title。我可以这样做:

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 为 NULL 的记录。如果在搜索参数中未指定 Title,我想包含 Title 为 NULL 的记录 - FirstName 和 LastName 相同。我知道我可能可以使用动态 SQL 来做到这一点,但我想避免这种情况。

【问题讨论】:

  • 尝试以下 where 语句:code ISNULL(FirstName, ') = ISNULL(@FirstName, '') - 这将使每个 NULL 为一个空字符串,并且可以通过 eq 进行比较。操作员。如果您想在输入参数为空的情况下获取所有标题,请尝试以下操作:codeFirstName = @FirstName OR @FirstName IS NULL。

标签: tsql optional-parameters


【解决方案1】:

根据给定参数动态更改搜索是一个复杂的主题,并且以一种方式进行而不是另一种方式,即使只有非常微小的差异,也会对性能产生巨大影响。关键是使用索引,忽略紧凑代码,忽略重复代码的担心,一定要做好查询执行计划(使用索引)。

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

Dynamic Search Conditions in T-SQL by by Erland Sommarskog

The Curse and Blessings of Dynamic SQL by Erland Sommarskog

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

OPTION (RECOMPILE) 添加到您的查询see Erland's article 中,SQL Server 将在根据本地变量的运行时值创建查询计划之前从(@LastName IS NULL OR LastName= @LastName) 中解析OR,并且可以创建索引用过。

这适用于任何 SQL Server 版本(返回正确的结果),但如果您使用的是 SQL 2008 SP1 CU5 (10.0.2746) 及更高版本,则仅包括 OPTION(RECOMPILE)。 OPTION(RECOMPILE) 将重新编译您的查询,只有列出的版本会根据本地变量的当前运行时值重新编译它,这将为您提供最佳性能。如果不在该版本的 SQL Server 2008 上,请不要使用该行。

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

【讨论】:

  • 小心 AND/OR 优先级。 AND 优先于 OR,因此如果没有正确的括号,此示例将不会产生预期的结果......所以它应该阅读: (@FirstName IS NULL OR (FirstName = @FirstName)) AND (@LastNameIS NULL OR (LastName= @LastName)) AND (@TitleIS NULL OR (Title= @Title))
  • ... (@FirstName IS NULL OR (FirstName = @FirstName) 应该是... (FirstName=Coalesce(@firstname,FirstName))
  • 不要忘记括号,否则将不起作用。
  • @fcm 这个想法是错误的:如果 FirstName 为 null,并且 @firstname 也为 null,则不会返回该行。因为 null 不等于 null。 sommarskog.se/dyn-search.html#coalesce
【解决方案2】:

以下情况可以这样做,

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

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

【讨论】:

    【解决方案3】:

    扩展您的WHERE 条件:

    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, '') = '')
    

    我。 e.将不同的情况与布尔条件结合起来。

    【讨论】:

      【解决方案4】:

      这也有效:

          ...
          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))
      

      【讨论】:

        【解决方案5】:

        就目前而言,@KM 的回答很好,但未能完全跟进他早期的建议之一;

        ...,忽略紧凑代码,忽略重复代码,...

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

        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
        

        这种方法的优势在于,在定制查询处理的常见情况下,查询尽可能高效 - 未提供标准不会产生影响。此外,索引和其他性能增强可以针对特定的定制查询,而不是试图满足所有可能的情况。

        【讨论】:

        • 当然最好为每种情况编写一个单独的存储过程。然后不用担心欺骗和重新编译。
        • 不用说,这种方法很快就会成为维护的噩梦。
        • @Atario 易于维护与性能是一种常见的权衡,这个答案是针对性能的。
        【解决方案6】:

        晚了五年。

        在提供的已接受答案的链接中提到了这一点,但我认为它应该得到一个明确的答案——根据提供的参数动态构建查询。例如:

        设置

        -- 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
        

        程序

        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
        

        用法

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

        优点:

        • 易写易懂
        • 灵活性 - 轻松生成查询以进行更复杂的过滤(例如动态 TOP)

        缺点:

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

        不是直接回答,而是与问题相关,即大局

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

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

            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;
            }
        

        优点:

        • 根据提供的过滤器动态生成查询。不需要 parameter sniffingrecompile 提示
        • 对于 OOP 领域的人来说更容易编写
        • 通常性能友好,因为将发出“简单”查询(但仍需要适当的索引)

        缺点:

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

        【讨论】:

        • 确保所有中间字符串都是 N'' 而不是 '' - 如果你的 SQL 超过 8000 个字符,你会遇到截断问题。
        • 此外,如果您拒绝了用户的直接 SELECT 权限,您可能需要在存储过程中添加“WITH EXECUTE AS OWNER”子句。但是,如果您使用此子句,请务必小心避免 SQL 注入。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-12-21
        • 1970-01-01
        • 1970-01-01
        • 2013-04-16
        相关资源
        最近更新 更多