【问题标题】:Stored Procedure is slower than on the fly query存储过程比动态查询慢
【发布时间】:2015-02-23 19:29:34
【问题描述】:

所以我有这个非常简单的 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

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

【问题讨论】:

  • 您应该拆分 4 种可能的情况以使用它们自己的查询或使用 OPTION (RECOMPILE)。第一个选项可能更可取,因为只有 4 种组合,而且您说它被频繁调用。
  • ExternalId 字段的确切数据类型是什么?它的输入参数定义为NVARCHAR(500)。虽然该字段的类型和大小可能是正确的,但索引它超过了 900 字节的限制,所以我怀疑它真的是一个 NVARCHAR(500) 字段。
  • 将参数@ExternalId 更改为varchar(60) 后,ExternalId 的数据类型为varchar(60) 对速度有任何影响。

标签: sql sql-server stored-procedures


【解决方案1】:

有几个因素有助于确定最佳行动方案:

  • ExternalId列的数据分布:

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

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

    其中一个NULL、特定值或任何值的频率如何?意思是,这个 proc 的 90% 的执行是来自 @IdNULL@ExternalId 5 个不同值之一吗?还是在 90% 的情况下传入了不同的 @Id 值?和/或是否有 1 个特定的 @ExternalId 值被使用或通常不同?

首先

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

选项

  • 使用OPTION (RECOMPILE): 这已经提到过,我将其包括在内是为了完整起见,并且说这应该​​是您的最后手段。此选项通过禁止您拥有任何缓存计划来确保您不会获得错误的缓存计划。这意味着您也永远无法从计划缓存中受益。在大多数情况下,会有更好的选择。

    AND,这在像您这样频繁执行存储过程的情况下非常重要:执行计划被缓存的原因是由于找出它们的成本,因此告诉 SQL Server一次又一次地弄清楚,对于每一次的执行,都会对过程产生影响。

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

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

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

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

    我在将这种技术与存储过程结合使用时取得了巨大的成功,这些存储过程每秒被调用几个小时,并且访问了多个表,每个表都有超过 1000 万行。这是在我最初尝试使用 OPTION (RECOMPILE) 却发现它让事情变得更糟之后。

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

注意事项

  • 当我说“糟糕的缓存计划”时,我指的是某些值的“糟糕”。执行计划在第一次执行存储过程时被缓存。它们被缓存直到 SQL Server 重新启动,或者一些执行DBCC FREEPROCCACHE,或者如果系统遇到内存压力并且需要释放一些内存用于查询,它可以转储一段时间未使用的计划。但是缓存的计划旨在成为它首先运行的参数值的最佳计划。后续执行中使用的不同值对于相同的计划可能会非常低效。所以“坏”是指 sometimes 条件,而不是 always 条件。如果计划总是很糟糕,那么很可能是查询本身而不是参数值。

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

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

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

【讨论】:

    【解决方案2】:

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

    语法是:

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

    但是,您的查询在where 子句中使用了or,这使得优化器根本难以使用索引。一种选择是切换到动态 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;
    

    【讨论】:

    • 使用OPTION (RECOMPILE) 的目的不是要在这里打败参数嗅探。这是为了利用“参数嵌入优化”行为,其中 SQL Server 甚至不需要生成适用于所有四种排列的计划。如果没有 PEO 行为,即使被嗅探的参数与正在执行的参数具有完全相同的值,该计划仍然可能是次优的,因为它需要在使用不同的参数值调用时工作。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多