由参数变量引起的查询速度慢,但为什么?

时间:2013-03-28 13:05:48

标签: performance sql-server-2008 select join parameters

我有3个版本的查询,最终会返回相同的结果。

当向一个相对较小的表添加额外的内连接时,其中一个变得非常慢,并且在where子句中使用了参数变量。

快速和慢速查询的执行计划非常不同(包含在每个查询下面)。

我想了解为什么会发生这种情况以及如何防止它。

此查询采用< 1秒。它没有额外的内连接,但它在where子句中使用参数变量。

    declare @start datetime = '20120115'
    declare @end datetime = '20120116'

    select distinct sups.campaignid 
    from tblSupporterMainDetails sups
    inner join tblCallLogs calls on sups.supporterid = calls.supporterid
    where calls.callEnd between @start and @end

  |--Parallelism(Gather Streams)
       |--Sort(DISTINCT ORDER BY:([sups].[campaignID] ASC))
            |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([sups].[campaignID]))
                 |--Hash Match(Partial Aggregate, HASH:([sups].[campaignID]))
                      |--Hash Match(Inner Join, HASH:([calls].[supporterID])=([sups].[supporterID]))
                           |--Bitmap(HASH:([calls].[supporterID]), DEFINE:([Bitmap1004]))
                           |    |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([calls].[supporterID]))
                           |         |--Index Seek(OBJECT:([GOGEN].[dbo].[tblCallLogs].[IX_tblCallLogs_callend_supporterid] AS [calls]), SEEK:([calls].[callEnd] >= '2012-01-15 00:00:00.000' AND [calls].[callEnd] <= '2012-01-16 00:00:00.000') ORDERED FORWARD)
                           |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([sups].[supporterID]))
                                |--Index Scan(OBJECT:([GOGEN].[dbo].[tblSupporterMainDetails].[AUTOGEN_IX_tblSupporterMainDetails_campaignID] AS [sups]),  WHERE:(PROBE([Bitmap1004],[GOGEN].[dbo].[tblSupporterMainDetails].[supporterID] as [sups].[supporterID],N'[IN ROW]')))

此查询采用&lt; 1秒。它有一个额外的内连接BUT在where子句中使用参数常量。

    select distinct camps.campaignid 
    from tblCampaigns camps
    inner join tblSupporterMainDetails sups on camps.campaignid = sups.campaignid
    inner join tblCallLogs calls on sups.supporterid = calls.supporterid
    where calls.callEnd between '20120115' and '20120116'

  |--Parallelism(Gather Streams)
       |--Hash Match(Right Semi Join, HASH:([sups].[campaignID])=([camps].[campaignID]))
            |--Bitmap(HASH:([sups].[campaignID]), DEFINE:([Bitmap1007]))
            |    |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([sups].[campaignID]))
            |         |--Hash Match(Partial Aggregate, HASH:([sups].[campaignID]))
            |              |--Hash Match(Inner Join, HASH:([calls].[supporterID])=([sups].[supporterID]))
            |                   |--Bitmap(HASH:([calls].[supporterID]), DEFINE:([Bitmap1006]))
            |                   |    |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([calls].[supporterID]))
            |                   |         |--Index Seek(OBJECT:([GOGEN].[dbo].[tblCallLogs].[IX_tblCallLogs_callend_supporterid] AS [calls]), SEEK:([calls].[callEnd] >= '2012-01-15 00:00:00.000' AND [calls].[callEnd] <= '2012-01-16 00:00:00.000') ORDERED FORWARD)
            |                   |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([sups].[supporterID]))
            |                        |--Index Scan(OBJECT:([GOGEN].[dbo].[tblSupporterMainDetails].[AUTOGEN_IX_tblSupporterMainDetails_campaignID] AS [sups]),  WHERE:(PROBE([Bitmap1006],[GOGEN].[dbo].[tblSupporterMainDetails].[supporterID] as [sups].[supporterID],N'[IN ROW]')))
            |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([camps].[campaignID]))
                 |--Index Scan(OBJECT:([GOGEN].[dbo].[tblCampaigns].[IX_tblCampaigns_isActive] AS [camps]),  WHERE:(PROBE([Bitmap1007],[GOGEN].[dbo].[tblCampaigns].[campaignID] as [camps].[campaignID],N'[IN ROW]')))

此查询需要2分钟。它有一个额外的内连接,它在where子句中使用参数变量。

    declare @start datetime = '20120115'
    declare @end datetime = '20120116'

    select distinct camps.campaignid 
    from tblCampaigns camps
    inner join tblSupporterMainDetails sups on camps.campaignid = sups.campaignid
    inner join tblCallLogs calls on sups.supporterid = calls.supporterid
    where calls.callEnd between @start and @end

  |--Nested Loops(Inner Join, OUTER REFERENCES:([camps].[campaignID]))
       |--Index Scan(OBJECT:([GOGEN].[dbo].[tblCampaigns].[IX_tblCampaigns_isActive] AS [camps]))
       |--Top(TOP EXPRESSION:((1)))
            |--Nested Loops(Inner Join, OUTER REFERENCES:([calls].[callID], [Expr1007]) OPTIMIZED WITH UNORDERED PREFETCH)
                 |--Nested Loops(Inner Join, OUTER REFERENCES:([sups].[supporterID], [Expr1006]) WITH UNORDERED PREFETCH)
                 |    |--Index Seek(OBJECT:([GOGEN].[dbo].[tblSupporterMainDetails].[AUTOGEN_IX_tblSupporterMainDetails_campaignID] AS [sups]), SEEK:([sups].[campaignID]=[GOGEN].[dbo].[tblCampaigns].[campaignID] as [camps].[campaignID]) ORDERED FORWARD)
                 |    |--Index Seek(OBJECT:([GOGEN].[dbo].[tblCallLogs].[IX_tblCallLogs_supporterID_closingCall] AS [calls]), SEEK:([calls].[supporterID]=[GOGEN].[dbo].[tblSupporterMainDetails].[supporterID] as [sups].[supporterID]) ORDERED FORWARD)
                 |--Clustered Index Seek(OBJECT:([GOGEN].[dbo].[tblCallLogs].[AUTOGEN_PK_tblCallLogs] AS [calls]), SEEK:([calls].[callID]=[GOGEN].[dbo].[tblCallLogs].[callID] as [calls].[callID]),  WHERE:([GOGEN].[dbo].[tblCallLogs].[callEnd] as [calls].[callEnd]>=[@s2] AND [GOGEN].[dbo].[tblCallLogs].[callEnd] as [calls].[callEnd]<=[@e2]) LOOKUP ORDERED FORWARD)

注意:

  • 我认为缓慢是由tblCallLogs上的聚集索引搜索引起的,但我不知道为什么SQL Server会选择此执行计划。
  • 我应该使用查询优化提示吗?我需要并且不愿意告诉SQL Server如何完成它的工作......
  • 问题似乎是由多种因素组合引起的 - 额外的连接和变量。
  • 执行计划在查找查询变量时是否可以尝试重用“错误”计划?
  • 在现实生活中,我将不得不使用参数变量。常数不好!所以在我的许多查询/存储过程中都可能存在这个问题!
  • 我在tblCampaignstblSupporterMainDetails重建了索引并更新了统计信息。这没有效果。
  • 两个表都在主键上具有聚簇索引(标识整数)。
  • 外键列campaignid已编入索引。
  • 所有查询都使用相同的参数值 - 将其用作变量或常量。

表格中的记录数:

  • tblSupporterMainDetails = 12,561,900
  • tblCallLogs = 27,242,224
  • tblCampaigns = 756

更新:

  • 我还在tblcalllogs重建了索引和更新的统计信息。没效果。
  • 我已使用DBCC FREEPROCCACHE
  • 清除了执行计划缓存
  • tblCallLogs.callEnd是一个日期时间。

涉及列的模式:

tblCampaign.campaignid int not null
tblSupporterMainDetails.campaignid int not null
tblSupporterMainDetails.supporterid int not null
tblCallLogs.supporterid int not null
tblCallLogs.callEnd datetime not null

索引:

Indexes

更新2: 将索引添加到tblCallLogs.supporterId后 - 使用include列:callEnd
'慢'查询加速最多40秒。 更新的执行计划:

  |--Nested Loops(Inner Join, OUTER REFERENCES:([camps].[campaignID]))
   |--Index Scan(OBJECT:([GOGEN].[dbo].[tblCampaigns].[IX_tblCampaigns_isActive] AS [camps]))
   |--Top(TOP EXPRESSION:((1)))
        |--Nested Loops(Inner Join, OUTER REFERENCES:([sups].[supporterID], [Expr1006]) WITH UNORDERED PREFETCH)
             |--Index Seek(OBJECT:([GOGEN].[dbo].[tblSupporterMainDetails].[AUTOGEN_IX_tblSupporterMainDetails_campaignID] AS [sups]), SEEK:([sups].[campaignID]=[GOGEN].[dbo].[tblCampaigns].[campaignID] as [camps].[campaignID]) ORDERED FORWARD)
             |--Index Seek(OBJECT:([GOGEN].[dbo].[tblCallLogs].[IX_tblCallLogs_supporterid_callend] AS [calls]), SEEK:([calls].[supporterID]=[GOGEN].[dbo].[tblSupporterMainDetails].[supporterID] as [sups].[supporterID]),  WHERE:([GOGEN].[dbo].[tblCallLogs].[callEnd] as [calls].[callEnd]>=[@s2] AND [GOGEN].[dbo].[tblCallLogs].[callEnd] as [calls].[callEnd]<=[@e2]) ORDERED FORWARD)

解:

额外的连接实际上并没有直接导致问题,但它显然改变了语句,以便sql server为它保留了不同的执行计划。
通过增加     OPTION(RECOMPILE) 在缓慢的声明结束时,我能够获得预期的快速性能。即&lt; 1秒。我仍然不确定这个解决方案是否有效 - 为什么没有冲洗所有的计划工作?这是参数嗅探的经典案例吗?我会在知道确切答案时更新这篇文章 - 或者直到有人能给出明确的答案。 感谢@LievenKeersmaekers和@JNK迄今为止的帮助......

1 个答案:

答案 0 :(得分:1)

导致解决方案的摘要:

supporterid, callEnd上添加覆盖索引。

这里的假设是优化器可以使用此索引(与callEnd,supporterid相比)

  • 首先加入tblSupporterMainDetailstblCallLogs
  • where子句中进一步使用它来选择callEnd

添加选项OPTION(RECOMPILE)

所有cudo用于TiborK和Hunchback,用于解释使用硬编码常量或变量的优化器的差异。

Performance Impact - Constant value -vs- Variable

  

使用常量时,优化器会知道该值   可以基于此确定选择性(和可能的索引使用)。   使用变量时,优化程序的值是未知的(所以它   必须通过一些硬连线值或可能密度信息)。所以,   从技术上讲,这不是参数嗅探,而是你的任何文章   找到那个主题也应该解释一下a之间的区别   常数和变量。使用OPTION(RECOMPILE)实际上会转向   参数嗅探情况的变化。

     

本质上,常量,变量之间存在很大差异   和一个参数(可以被嗅探)。