存储过程比在线查询慢

时间:2014-12-26 13:38:24

标签: sql sql-server stored-procedures

所以我有这个非常简单的SP GetData,它有两个参数,看起来像这样

SET ANSI_NULLS ON
GO
SET QOUTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[GetData]
    @Id int = NULL,
    @ExternalId nvarchar(500) = NULL
AS
BEGIN

    SET NOCOUNT ON;
    SELECT
        t.Id,
        t.ExternalId,
        t.Column1 -- other columns ommited for brevity
    FROM [SomeTable] t
    WHERE
        (t.Id = @Id OR @ID IS NULL) AND
        (t.ExternalId = @ExternalId OR @ExternalId IS NULL)

END

这非常简单。只需一个表中的一个select语句。现在,我关心的是,如果我执行此过程,平均花费的时间是0.505369秒,但如果我提取该选择查询并执行它,则查询平均需要0.023923秒。我真的很担心这个问题,因为这个过程实际上经常被称为并且是我应用程序中的一个关键步骤,这就是为什么它现在最小化为0.5s有点可以接受。这次表只包含495万行。 Id列上有聚簇索引,ExternalId列上有非聚集索引。该表应该会在即将到来的弱点中增加到4500万行,而且数据插入率会降低。在4500万行中,我不认为上面显示的SP会给出一些合理的时间。我真的不知道这里的问题是什么,或者它应该是那样的?据我所知,在执行SP之后,计划被缓存,下次计划没有重新创建,所以它应该比on the fly query更快吗?在这种情况下,最好使用Ad hoc查询而不是SP?数据库是Sql Server 2012.提前感谢

2 个答案:

答案 0 :(得分:5)

我的第一个建议是使用“重新编译”选项。发生的事情是在第一次运行存储过程时编译查询(这称为“参数嗅探”)。如果第一次运行的执行路径与最佳执行路径不同,这可能会对性能产生影响。例如,有时存储过程在超小型表上进行测试,因此索引不会被使用。

语法为:

SELECT
    t.Id,
    t.ExternalId,
    t.Column1 -- other columns omited for brevity
FROM [SomeTable] t
WHERE
    (t.Id = @Id OR @ID IS NULL) AND
    (t.ExternalId = @ExternalId OR @ExternalId IS NULL)
OPTION (recompile);

但是,您的查询在or子句中使用where,这使得优化器很难使用索引。一种选择是切换到动态SQL,如下所示:

declare @sql nvarchar(max) = '
SELECT t.Id, t.ExternalId,
       t.Column1 -- other columns omited for brevity
FROM [SomeTable] t
WHERE 1=1 ';
set @sql = @sql + (case when @id is not null then ' and t.Id = @id'` else '' end) +
           (case when @ExternalId is not null then ' and t.externalId = @externalId' else '' end);

exec sp_executesql @sql, N'@Id int, @ExternalId int', @id = @id, @externalId = @externalId;

答案 1 :(得分:1)

有几个因素可以帮助确定最佳行动方案:

  • ExternalId列中的数据分布:

    如果ExternalId值相当均匀地展开,那么在可能的NULL值之外甚至不使用该字段,一个值不应产生不适用于其他值的计划。你不需要担心Id字段,假设它是PK,因为PK的本质是每个值只有1个。

  • 输入参数值的实际可变性:

    其中一个NULL,特定值或任何值的频率是多少?这意味着,这个触发的执行中有90%来自@Id NULL@ExternalId 5个不同值中的一个?或者是90%的时间传递了不同的@Id值?和/或是否使用了@ExternalId的1个特定值,或者它通常是不同的?

第一

在考虑任何结构更改之前,请确保ExternalId字段的数据类型与@ExternalId输入参数的数据类型匹配。 @ExternalId输入参数定义为NVARCHAR(500),因此如果ExternalId字段实际上声明为VARCHAR,那么您可能正在进行"索引扫描&#34 ;而不是索引寻求"由于从VARCHARNVARCHAR的隐式转换。

选项

  • 使用OPTION (RECOMPILE)这已经提到了,为了完整起见,我将其包括在内并说这应该是你的 last

    AND ,这在像你这样频繁执行存储过程的情况下非常重要:缓存执行计划的原因是由于计算它们的费用,因此告诉SQL Server为了每次执行,一次又一次地解决这个问题,将会对这个过程造成影响。

  • 使用OPTION (OPTIMIZE FOR...)此选项可让您告知查询优化器根据当前统计信息假设所有输入参数的平均分布(使用时{{1或者假设基于一个或多个输入参数的特定值的分布(使用OPTIMIZE FOR UKNOWN时)。请注意,您仍然可以使用OPTIMIZE FOR ( @variable_name { UNKNOWN | = literal_constant } [ , ...n ] )关键字来假设特定参数的平均分布,同时还使用其他参数的特定值。有关详细信息,请参阅Query Hints的MSDN页面。

  • 参数化动态SQL(即UNKNOWN):此选项解决了各种参数组合的问题(您尝试使用'Field = @Param'方法解决这些问题)。如果Field = @Param OR @Param IS NULL字段中的数据分布相当均匀,那么这可能就是您所需要的。但如果它非常不平衡,那么你仍然可能会遇到一个糟糕的缓存计划的问题。

  • 文字(即非参数化)动态SQL(即ExternalId):在此方法中,您可以将适当的参数值连接到动态SQL中(确保'Field = ' + CONVERT(NVARCHAR(50), @Param)中没有任何单引号,以避免SQL注入)。这将为您提供针对特定值定制的查询计划,并且如果再次传入这些值(在两个输入参数的精确组合中),则可以重复使用该查询计划。这里的主要缺点是,如果输入参数传入的值的变化很大,您将生成相当多的执行计划,并且它们会占用内存。但是在数据分布高度变化的情况下(即一个@ExternalId拉出50行,而另一个值拉动200万),那么这可能就要走了。

  • 参数化和非参数化动态SQL的组合:如果输入参数的值变化很大但表中的数据分布相当均匀,那么可以在动态SQL中参数化此参数,同时在具有高度变化的数据分布的输入参数中进行连接。当然,在这种特殊情况下,我们知道@ExternalId分布非常均匀,所以如果Id也是均匀分布的,那么你应该坚持使用参数化动态SQL(如上所述)。与使用完全Literal选项相比,这将导致更少的执行计划。

    我使用这种技术获得了巨大的成功,存储过程每秒被调用几个小时,而且每个表都有超过1000万行。这是在我最初尝试使用ExternalId之后才发现它使事情变得更糟。

  • 多个存储过程:假设您没有同时调用此proc同时输入参数为OPTION (RECOMPILE),则可以创建三个存储过程对于以下组合:NULL - 仅限@Id - 仅限@ExternalId@Id。然后由应用程序代码决定执行哪个存储过程。由于数据均匀分布,这似乎对@ExternalId - 仅proc很有用。但是,根据@Id的值的均匀性或不均匀性的分布情况,具有ExternalId输入参数的两个存储过程仍然可能遇到获取错误的缓存计划的问题。

备注

  • 当我说"错误的缓存计划"时,我的意思是"糟糕"一些价值观。执行计划在第一次执行存储过程时被缓存。它们被缓存直到SQL Server重新启动,或者一些执行@ExternalId,或者如果系统遇到内存压力并且需要释放一些内存用于查询,它可以转储尚未使用的计划等一下。但是,缓存的计划旨在成为首次运行的参数值的最佳计划。在后续执行中使用的不同值可能在同一计划中非常低效。所以"坏"是指有时条件,而不是始终条件。如果计划总是不好,那么查询本身很可能是问题,而不是参数值。

  • 动态SQL的缺点是它打破了所有权链接。这意味着,通常需要向用户授予直接表权限,因为不能通过存储过程的所有者假定权限。然而,好消息是,您不需要向执行存储过程的用户授予直接表权限。使用动态SQL时,可以执行以下操作以保持适当的安全性:

    我假设我们只处理单个数据库。

    • 在数据库中创建证书
    • 根据该证书创建用户
    • 将表权限授予此新的基于证书的用户
    • 使用ADD SIGNATURE对存储过程进行签名,该存储过程有效地授予存储过程 - 而不是用户执行存储过程 - 分配给的权限新的基于证书的用户。