在proc之外,相同的查询运行得更快

Geo*_*tis 6 sql t-sql sql-server stored-procedures sql-execution-plan

我们有一个特定的查询,在proc内部运行得慢得多.我必须在这里添加它,它被包含在一个两级游标中.但是,两个游标都有一行的迭代结果集.

让我先说明我们尝试过但失败的事情:

  • 使用选项(重新编译)和选项(optiimize for(@var UNKNOWN)避免参数嗅探
  • 这个帖子.似乎是问题的变量实际上是本地变量而不是proc参数.

这是从proc/cursors内部获取的查询.

 select @tpdim1 = dim1, @tpdim2 = dim2, @typecalc = typecalc
    from loyalty_policy where code=@loop2_loyalty_policy
Run Code Online (Sandbox Code Playgroud)

注意:@ loop2_loyalty_policy是取自内部游标结果的var,并且有一个值.code是PK到loyalty_policy桌子.因此,@ tpdim1和@tpdim2各有一个值.

SET STATISTICS PROFILE ON 
SET STATISTICS    xml on           
                  insert into @tbl_loyal_loop2 (cnt, store, map, pda, insdate, line, item, loyalty_policy_data, loyal_calc, loyalty_policy)
                  select @cnt, t.store, t.map, t.pda, t.insdate, t.line, t.item, ld.tab_id,  
                  case @typecalc
                        when 1 then convert(bigint,round(isnull(t.valueFromTran,0.00) * ld.value , 0 ) )
                        when 2 then convert(bigint,round(isnull(t.netvalue,0.00) * ld.value , 0 ) )
                        when 3 then convert(bigint,isnull(t.qty,0) * ld.value )
                        when 4 then convert(bigint,round(isnull(t.valueFromPrice2,0.00) * ld.value , 0 ) )
                        when 5 then convert(bigint,round(isnull(t.valueFromPrice3,0.00) * ld.value , 0 ) )
                        when 6 then convert(bigint,round(isnull(t.valueFromPrice4,0.00) * ld.value , 0 ) )
                  else 0 end
                  ,@loop2_loyalty_policy
                  from loyalty_policy_data ld-- with (index=ind_loyalty_policy_02)
                              inner join #tbl_data t on t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive
                  where ld.loyalty_policy = @loop2_loyalty_policy 
                  and ld.tdateactive >= @from_rundate and ld.fdateactive <= @to_rundate
                  and t.dbupddate > @loop1_dbupddate  
                  and
                        case when @tpdim1 is null then '' 
                        else  
                              case  @tpdim1 
                                    when 'STORE'            then t.store when 'BRAND' then t.brand  when 'CAT1' then t.cat1   when 'CAT2' then t.cat2   when 'CAT3' then t.cat3   when 'ITEM' then t.item    
                                    when 'CUSTGROUP'  then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
                                    when 'CUSTOMER'         then @customer
                              else '' end
                        end
                        = case when @tpdim1 is null then '' else ld.dim1 end
                  and 
                        case when @tpdim2 is null then '' 
                        else  
                              case  @tpdim2 
                                    when 'STORE'            then t.store when 'BRAND' then t.brand  when 'CAT1' then t.cat1   when 'CAT2' then t.cat2   when 'CAT3' then t.cat3   when 'ITEM' then t.item    
                                    when 'CUSTGROUP'  then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
                                    when 'CUSTOMER'         then @customer                     
                              else '' end
                        end
                        = case when @tpdim2 is null then '' else ld.dim2 end
SET STATISTICS    xml off    
Run Code Online (Sandbox Code Playgroud)

SET STATISTICS XML对以上的回报这个计划.

在尝试调试它时,我们以下面的形式隔离了查询(在这里,您还可以看到表#a是如何制作的,它与之前的#tbl_data具有完全相同的数据):

drop table #a;
select dt.dbupddate, dt.insdate, dt.map, dt.pda, pt.line, pt.item, 
( pt.exp_qty - pt.imp_qty)  as qty,  
( pt.exp_value + pt.imp_value )  as netvalue, 
( (document.exp_val - document.imp_val) * (pt.netvalue - pt.vat_value) )  as valueFromTran,  
( (document.exp_val - document.imp_val) * ( ( (pt.qty - pt.qty_gift) * isnull(pt.price2,0.00) ) * (1.00-( pt.disc_perc / 100)) ) ) as valueFromPrice2, 
( (document.exp_val - document.imp_val) * ( ( (pt.qty - pt.qty_gift) * isnull(pt.price3,0.00) ) * (1.00-( pt.disc_perc / 100)) ) ) as valueFromPrice3, 
( (document.exp_val - document.imp_val) * ( ( (pt.qty - pt.qty_gift) * isnull(pt.price4,0.00) ) * (1.00-( pt.disc_perc / 100)) ) ) as valueFromPrice4, 
dt.store, item.brand, item.cat1, item.cat2, item.cat3, customer.custgroup, customer.custgroup2, customer.custgroup3 
into #a
from document with (nolock) 
      inner join dt with (nolock) on dt.doccode = document.code 
      inner join store with (nolock) on store.code = dt.store and store.calc_loyal = 1 
      inner join customer with (nolock) on customer.code = dt.customer  
      inner join pt with (nolock) on dt.map = pt.map and dt.pda=pt.pda 
      inner join item with (nolock) on item.code = pt.item and item.itemtype in (select code from itemtype with (nolock) where vsales = 1)
where dt.canceled = 0 and document.is_opposite = 0 and document.type = 3 and dt.customer=N'EL4444444'
and dt.insdate >= '20180109' and dt.insdate <= '20190108' ;



SET STATISTICS PROFILE ON 
                  select t.store, t.map, t.pda, t.insdate, t.line, t.item, ld.tab_id,  
                  case 4
                        when 1 then convert(bigint,round(isnull(t.valueFromTran,0.00) * ld.value , 0 ) )
                        when 2 then convert(bigint,round(isnull(t.netvalue,0.00) * ld.value , 0 ) )
                        when 3 then convert(bigint,isnull(t.qty,0) * ld.value )
                        when 4 then convert(bigint,round(isnull(t.valueFromPrice2,0.00) * ld.value , 0 ) )
                        when 5 then convert(bigint,round(isnull(t.valueFromPrice3,0.00) * ld.value , 0 ) )
                        when 6 then convert(bigint,round(isnull(t.valueFromPrice4,0.00) * ld.value , 0 ) )
                  else 0 end
                  ,'003'
                  --select count(*)
                  from loyalty_policy_data ld with (index=ind_loyalty_policy_02)
                              inner join #a t on t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive
                  where ld.loyalty_policy = '003' 
                  --and ld.tdateactive >= '20180109' and ld.fdateactive <= '20190108'
                  and t.dbupddate > '20000101'
      and 
                        case when 'CUSTOMER' is null then '' 
                        else  
                              case  'CUSTOMER' 
                                    when 'STORE'            then t.store when 'BRAND' then t.brand  when 'CAT1' then t.cat1   when 'CAT2' then t.cat2   when 'CAT3' then t.cat3   when 'ITEM' then t.item    
                                    when 'CUSTGROUP'  then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
                                    when 'CUSTOMER'         then 'EL0134366'
                              else '' end
                        end
                        = case when 'CUSTOMER' is null then '' else ld.dim1 end
                  and 
                        case when 'BRAND' is null then '' 
                        else  
                              case  'BRAND' 
                                    when 'STORE'            then t.store when 'BRAND' then t.brand  when 'CAT1' then t.cat1   when 'CAT2' then t.cat2   when 'CAT3' then t.cat3   when 'ITEM' then t.item    
                                    when 'CUSTGROUP'  then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
                                    when 'CUSTOMER'         then 'EL0134366'

                              else '' end
                        end
                        = case when 'BRAND' is null then '' else ld.dim2 end
SET STATISTICS PROFILE off    
Run Code Online (Sandbox Code Playgroud)

这里是执行计划.这样运行得更快.

为什么这个巨大的差异?根据我对执行分析的有限知识,我注意到了

  1. 关于index spool操作的第一个(慢)查询估计行为~9700但实际行为300万.
  2. 第二个查询使用了许多具有并行性的操作
  3. 我在第二个查询中看到的唯一"真正"差异是@tpdim1和@tpdim2值的手动替换值.果然,当我们进入第一个查询的proc代码,并用他们应该获得的单个值替换@tpdim1和@tpdim2时,它的运行速度与第二个查询一样快.

你能否解释一下这个差异并提出一些建议来解决这个问题?


编辑:正如Laughing Vergil推荐的那样,我用先前声明的变量替换了第二个查询中的文字,并再次运行缓慢!


编辑2:我做了一些进一步研究的其他信息.

首先,我已将问题隔离到这一行:

case when @tpdim1 is null then '' < - 这使用慢速计划

case when 'CUSTOMER' is null then '' < - 这使用快速计划

在ad-hoc查询中也是如此,不需要用spcs和/或游标来解决问题.

即使我将代码更改为推荐的动态结构,这仍然会产生影响.

我还没有创建任何sampla数据,但重要的信息(如计划中所示)是loyalty_policy_data大约720k行,如果我们只过滤loyalty_policy = @loop2_loyalty_policy.但是,如果我们评估@tpdim1条件,基本上是dim1 = N'EL0134366',则返回的行只有4.

然后,计划的差异是在评估日期检查条件时评估该条件.

在快速计划中,首先评估它 - 当寻求忠诚度策略值的索引时,它会添加一个(非寻道)谓词.虽然此谓词不在索引中,但返回的行为4,而所有其他运算符都具有"逻辑"大小.

相比之下,缓慢的计划痛苦地忽视了这个谓词,直到为时已晚.如果我的想法正确,它会将loyalty_policy_data上的嵌套循环作为外表(这很疯狂).它将所需的列作为外部引用传递.对于每个这样的元组,索引假脱机扫描#table(~1k行)并找到大约250个结果,并将其传递给最终执行tpdim1过滤的过滤器.因此,250*700k行传递给过滤器运算符.

所以现在我想我知道会发生什么.但我无法理解为什么.

Vla*_*nov 2

回答你的问题:

\n\n
\n

清晰且可重复地解释查询分析器在这些情况下的行为方式和原因\n

\n
\n\n

在这些情况下,查询优化器的行为有所不同,因为带有变量的计划必须对于任何可能的未来参数值都有效,因此优化器会生成一个复杂的通用计划,即使参数为 NULL,该计划也会产生正确的结果。

\n\n

带有文字(而不是变量)的计划通常更有效,因为优化器可以CASE在计划编译阶段极大地简化您的逻辑。优化器有更好的机会选择最佳计划形状,因为当查询更简单并且过滤器具有已知值时,优化器更容易考虑有关索引和基数估计的可用信息。

\n\n
\n\n

Martin Smith在评论中指出,您使用的服务器版本是10.0.2531.0,即2008 SP1,并且没有启用参数嵌入优化。您至少需要该分支上的SP1 CU5OPTION (RECOMPILE)才能正常工作(正如我期望它在下面的说明中工作)。

\n\n

Erland Sommarskog 在下面提到的他的文章中也谈到了这一点。他说你至少需要使用 SP2。

\n\n

如果无法更新服务器,请查看旧版 Erland 的文章Dynamic Search Conditions in T\xe2\x80\x91SQL Version for SQL 2005 and Early看看如何处理这种情况OPTION (RECOMPILE)。无法使用。

\n\n
\n\n

这是我原来的答案。

\n\n

我知道你说过你已经尝试过,但我仍然请你仔细检查。查看您的症状OPTION (RECOMPILE)应该会有所帮助。

\n\n

您需要将此选项添加到主查询中。不是整个存储过程。像这样:

\n\n
insert into @tbl_loyal_loop2 (cnt, store, map, pda, insdate, line, item, loyalty_policy_data, loyal_calc, loyalty_policy)\nselect @cnt, t.store, t.map, t.pda, t.insdate, t.line, t.item, ld.tab_id,  \ncase @typecalc\n    when 1 then convert(bigint,round(isnull(t.valueFromTran,0.00) * ld.value , 0 ) )\n    when 2 then convert(bigint,round(isnull(t.netvalue,0.00) * ld.value , 0 ) )\n    when 3 then convert(bigint,isnull(t.qty,0) * ld.value )\n    when 4 then convert(bigint,round(isnull(t.valueFromPrice2,0.00) * ld.value , 0 ) )\n    when 5 then convert(bigint,round(isnull(t.valueFromPrice3,0.00) * ld.value , 0 ) )\n    when 6 then convert(bigint,round(isnull(t.valueFromPrice4,0.00) * ld.value , 0 ) )\nelse 0 end\n,@loop2_loyalty_policy\nfrom loyalty_policy_data ld -- with (index=ind_loyalty_policy_02)\n            inner join #tbl_data t on t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive\nwhere ld.loyalty_policy = @loop2_loyalty_policy \nand ld.tdateactive >= @from_rundate and ld.fdateactive <= @to_rundate\nand t.dbupddate > @loop1_dbupddate  \nand\n    case when @tpdim1 is null then \'\' \n    else  \n            case  @tpdim1 \n                when \'STORE\'            then t.store when \'BRAND\' then t.brand  when \'CAT1\' then t.cat1   when \'CAT2\' then t.cat2   when \'CAT3\' then t.cat3   when \'ITEM\' then t.item    \n                when \'CUSTGROUP\'  then t.custgroup when \'CUSTGROUP2\' then t.custgroup2 when \'CUSTGROUP3\' then t.custgroup3\n                when \'CUSTOMER\'         then @customer\n            else \'\' end\n    end\n    = case when @tpdim1 is null then \'\' else ld.dim1 end\nand \n    case when @tpdim2 is null then \'\' \n    else  \n            case  @tpdim2 \n                when \'STORE\'            then t.store when \'BRAND\' then t.brand  when \'CAT1\' then t.cat1   when \'CAT2\' then t.cat2   when \'CAT3\' then t.cat3   when \'ITEM\' then t.item    \n                when \'CUSTGROUP\'  then t.custgroup when \'CUSTGROUP2\' then t.custgroup2 when \'CUSTGROUP3\' then t.custgroup3\n                when \'CUSTOMER\'         then @customer                     \n            else \'\' end\n    end\n    = case when @tpdim2 is null then \'\' else ld.dim2 end\nOPTION(RECOMPILE);\n
Run Code Online (Sandbox Code Playgroud)\n\n

OPTION (RECOMPILE)并不是为了减轻参数嗅探,而是为了允许优化器将参数的实际值内联到查询中。这使优化器可以自由地简化查询逻辑。

\n\n

您的查询类型类似于动态搜索条件,我强烈建议您阅读 Erland Sommarskog 撰写的那篇文章。

\n\n

另外,代替

\n\n
and\n    case when @tpdim1 is null then \'\' \n    else  \n            case  @tpdim1 \n                when \'STORE\'            then t.store when \'BRAND\' then t.brand  when \'CAT1\' then t.cat1   when \'CAT2\' then t.cat2   when \'CAT3\' then t.cat3   when \'ITEM\' then t.item    \n                when \'CUSTGROUP\'  then t.custgroup when \'CUSTGROUP2\' then t.custgroup2 when \'CUSTGROUP3\' then t.custgroup3\n                when \'CUSTOMER\'         then @customer\n            else \'\' end\n    end\n    = case when @tpdim1 is null then \'\' else ld.dim1 end\n
Run Code Online (Sandbox Code Playgroud)\n\n

我会写得有点不同:

\n\n
and\n(\n    @tpdim1 is null\n    OR\n    (\n            ld.dim1 =\n            case @tpdim1\n                when \'STORE\'      then t.store \n                when \'BRAND\'      then t.brand  \n                when \'CAT1\'       then t.cat1   \n                when \'CAT2\'       then t.cat2   \n                when \'CAT3\'       then t.cat3   \n                when \'ITEM\'       then t.item    \n                when \'CUSTGROUP\'  then t.custgroup \n                when \'CUSTGROUP2\' then t.custgroup2 \n                when \'CUSTGROUP3\' then t.custgroup3\n                when \'CUSTOMER\'   then @customer\n                else \'\'\n            end\n    )\n)\n
Run Code Online (Sandbox Code Playgroud)\n\n

当has a value of和has a value ofOPTION (RECOMPILE)时,优化器应该将此语句转换为简单的@tpdim1CUSTOMER@customerEL0134366

\n\n
and\n(\n    ld.dim1 = `EL0134366`\n)\n
Run Code Online (Sandbox Code Playgroud)\n\n

然后它将能够使用合适的索引或更准确地估计行数,并对计划形状做出更好的决策。使用此选项,计划将仅对参数的该特定值有效。

\n\n

请注意,这option (optimize for UNKNOWN)在这里没有帮助。optimize for UNKNOWN必须生成一个对任何可能的参数值都有效的通用计划。

\n

  • OP 的版本为“10.0.2531.0”,这是 2008 SP1,并且没有启用参数嵌入优化。它需要[该分支上至少有 CU5](https://blogs.msdn.microsoft.com/grahamk/2009/11/18/changed-behaviour-of-option-recompile-syntax-in-sql-server- 2008-sp1-累积更新-5/) (2认同)