字段 = 参数 OR 参数是 NULL 模式

M. *_*son 7 sql-server execution-plan plan-cache

我知道与使用如下谓词编写的存储过程相关的参数嗅探问题:

CREATE PROCEDURE [dbo].[Get] @Parameter INT = NULL AS BEGIN;
    SELECT [Field] FROM [dbo].[Table]
    WHERE [Field] = @Parameter 
    OR @Parameter IS NULL;
END;
Run Code Online (Sandbox Code Playgroud)

根据参数的值,第一次执行时是标量还是 NULL,一个计划被缓存,对于相反的值可能是次优的。

假设 [Field] 是标量,并且是表上的聚簇索引。以下编写存储过程以支持查询的方法的优缺点是什么:

同一个存储过程中的条件选择

CREATE PROCEDURE [dbo].[Get] @Parameter INT = NULL AS BEGIN;
    IF(@Parameter IS NOT NULL) BEGIN;
        SELECT [Field]
        FROM [dbo].[Table]
        WHERE [Field] = @Parameter;
    END;
    ELSE BEGIN;
        SELECT [Field]
        FROM [dbo].[Table];
    END;
END;
Run Code Online (Sandbox Code Playgroud)

存储过程中的动态 SQL

CREATE PROCEDURE [dbo].[Get] @Parameter INT = NULL AS BEGIN;
    DECLARE @sql NVARCHAR(MAX) = N'';
    SET @sql += N'SELECT [Field]'
    SET @sql += N'FROM [dbo].[Table]';

    IF(@Parameter IS NOT NULL) BEGIN;
        @sql += N'WHERE [Field] = @Parameter';
    END;

    SET @sql += N';';

    EXEC sp_executesql @sql N'@Parameter INT', @Parameter;
END;
Run Code Online (Sandbox Code Playgroud)

独立的存储过程

CREATE PROCEDURE [dbo].[Get] @Parameter INT = NULL AS BEGIN;
    SELECT [Field]
    FROM [dbo].[Table]
    WHERE [Field] = @Parameter;
END;

CREATE PROCEDURE [dbo].[GetAll] AS BEGIN;
    SELECT [Field]
    FROM [dbo].[Table];
END;
Run Code Online (Sandbox Code Playgroud)

Rob*_*ley 5

他们都很优秀。真的。它们都具有在缓存中有两个计划的相同影响,这正是您想要的。

随着你得到越来越多的参数,你会发现动态 SQL 选项是最清晰的,尽管它对初学者来说看起来更可怕。

如果这是一个函数,我建议避免使用多语句选项,以便 QO 可以更好地完成它的工作。


Mar*_*ith 5

如果您使用的是最近版本的 SQL Server,则可以考虑的另一种选择是

SELECT [Field]
FROM   [dbo].[Table]
WHERE  [Field] = @Parameter
        OR @Parameter IS NULL
OPTION (RECOMPILE); 
Run Code Online (Sandbox Code Playgroud)

每次都以每次编译为代价获得运行时值的最佳计划。

您的“条件选择”选项仍然容易受到参数嗅探的影响。如果程序第一次执行时@Parameter为 null则带有[Field] = @Parameter谓词的分支将估计 1 行(从=NULL谓词预期的 0 向上舍入)。

在您的问题中的特定示例中,您选择了一个列并且与您正在过滤的列相同,这不太可能出现问题,但在其他情况下可能会出现问题。

例如,在下面的例子中,第一次调用[dbo].[Get] 1需要 333,731 次逻辑读取,因为它选择了一个不合适的带有键查找的计划。当计划从缓存中删除并重新编译时,1逻辑读取下降到 4,330

DROP TABLE IF EXISTS [Table]

GO

CREATE TABLE [Table]
(
[Field1]  INT INDEX IX,
[Field2]  INT,
[Field3]  INT,
);

INSERT INTO [Table]
SELECT TOP 1000000 CRYPT_GEN_RANDOM(1)%3, CRYPT_GEN_RANDOM(4), CRYPT_GEN_RANDOM(4)
FROM sys.all_objects o1, sys.all_objects o2

GO

CREATE OR ALTER PROCEDURE [dbo].[Get] @Parameter INT = NULL AS BEGIN;
    IF(@Parameter IS NOT NULL) BEGIN;
        SELECT *
        FROM [dbo].[Table]
        WHERE [Field1] = @Parameter;
    END;
    ELSE BEGIN;
        SELECT *
        FROM [dbo].[Table];
    END;
END;

GO

SET STATISTICS TIME ON
SET STATISTICS IO ON


EXEC [dbo].[Get] 

EXEC [dbo].[Get] 1;

declare @plan_handle varbinary(64) = (select plan_handle from sys.dm_exec_procedure_stats where object_id = object_id('[dbo].[Get]'));

--Remove the plan from the cache 
DBCC FREEPROCCACHE (@plan_handle);  

--Re-execute it with NOT NULL passed first
EXEC [dbo].[Get] 1;
Run Code Online (Sandbox Code Playgroud)


M. *_*son 2

基于 Aaron Bertrand、Martin Smith 和 Rob Farley 之前的回答和评论。我想为每种方法列出一个赞成/反对列表,包括附加方法选项(重新编译):


同一存储过程中的条件选择

来自马丁·史密斯的回应:

您的“条件选择”选项仍然容易受到参数嗅探的影响。如果在 @Parameter 为 null 时首次执行该过程,则带有 [Field] = @Parameter 谓词的分支将估计 1 行(从 =NULL 谓词预期的 0 向上舍入)。

  • 无重新编译成本。
  • 为每个语句和存储过程规划缓存重用。
  • 即使当 @Parameter 不为 NULL 时结果集中没有显着差异,缓存的计划也容易受到参数嗅探的影响。
  • 随着参数数量的增加,管理上无法很好地扩展。
  • 所有 T-SQL 上的智能感知。

存储过程中的动态 SQL

来自罗布·法利:

当你获得越来越多的参数时,你会发现动态 SQL 选项是最清晰的,尽管它对于初学者来说看起来更可怕。

  • 无重新编译成本。
  • 为每个语句和存储过程规划缓存重用。
  • 仅当 @Parameter 不为 NULL 且结果集中存在显着差异时,缓存计划才容易受到参数嗅探的影响。
  • 随着参数数量的增加,管理上可以很好地扩展。
  • 不为所有 T-SQL 提供 Intellisense。

单独的存储过程

  • 无重新编译成本。
  • 为每个语句和存储过程规划缓存重用。
  • 仅当 @Parameter 不为 NULL 且结果集中存在显着差异时,缓存计划才容易受到参数嗅探的影响。
  • 随着参数数量的增加,管理上无法很好地扩展。
  • 所有 T-SQL 上的智能感知。

选项(重新编译)

来自马丁·史密斯:

每次都以每次编译为代价获得运行时值的最佳计划。

  • 重新编译的 CPU 成本。
  • 对于后面跟有 OPTION(RECOMPILE) 的语句,没有计划缓存重用,只有存储过程和不带 OPTION(RECOMPILE) 的语句。
  • 随着参数数量的增加,管理上可以很好地扩展。
  • 不容易受到参数嗅探的影响。
  • 所有 T-SQL 上的智能感知。

我的个人收获

如果 @Parameter 的不同标量值的结果集没有显着差异,则动态 SQL 性能最佳,系统开销最小,并且与 OPTION(RECOMPILE) 相比,在管理开销方面仅稍差一些。在更复杂的场景中,参数值的变化可能会导致结果集发生重大变化,使用带有条件包含或排除 OPTION(RECOMPILE) 的动态 SQL 将是最佳的整体性能。以下是 Aaron Bertrand 详细介绍该方法的文章的链接:https ://blogs.sentryone.com/aaronbertrand/backtobasics-updated-kitchen-sink-example/