不在proc

时间:2018-12-03 17:13:08

标签: sql sql-server tsql stored-procedures sql-execution-plan

我们有一个特定的查询,在proc中运行时会慢很多。我必须在这里添加它,它包含在两个级别的游标中。但是,两个游标的迭代结果集都只有一行。

让我先说一下我们尝试过和失败的事情:

  • 通过使用选项(重新编译)和选项(针对(@var UNKNOWN)进行优化)来避免参数嗅探
  • This thread。似乎是问题的变量实际上是局部变量,而不是proc参数。

这是从proc /游标内部获取的查询。

 select @tpdim1 = dim1, @tpdim2 = dim2, @typecalc = typecalc
    from loyalty_policy where code=@loop2_loyalty_policy

注意:@ loop2_loyalty_policy是从内部游标的结果中获取的var,并且具有一个值。 codeloyalty_policy表的PK。因此,@ 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    

上述SET STATISTICS XML返回this plan

在尝试调试它时,我们以以下形式隔离了查询(在这里,您还可以查看表#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    

here是执行计划。这样可以更快地运行很多。

为什么会有如此巨大的差异?从我对执行分析的有限知识中,我已经注意到

  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 '' <-这使用快速计划

这在即席查询中是正确的,无需麻烦自己使用spcs和/或游标。

即使我将代码更改为建议的动态where结构,这也会使您感到困惑。

我还没有创建任何样本数据,但是重要的信息(如计划所示)是,如果我们仅按loyalty_policy_data进行过滤,则loyalty_policy = @loop2_loyalty_policy大约有72万行。但是,如果我们评估@ tpdim1条件(本质上是dim1 = N'EL0134366'),则返回的行仅为4。

那么,计划的区别在于何时根据日期检查条件评估了此条件。

在快速计划中,它首先得到评估-在寻找忠诚度策略值的索引时,它会添加一个(非寻找)谓词。尽管此谓词不在索引中,但返回的行为4,其他所有运算符的大小均为“逻辑”。

相反,缓慢的计划痛苦地无视了这个谓词,直到为时已晚。如果我没有弄错,它将对loyalty_policy_data作为外部表进行嵌套循环(这很疯狂)。它将所需的列作为外部引用传递。对于每个这样的元组,索引假脱机扫描#table(〜1k行)并找到约250个结果,并将其传递给过滤器,该过滤器最终进行tpdim1过滤。因此,将250 * 700k行传递给了筛选器运算符。

所以现在我想我知道会发生什么。但是我不知道为什么。

3 个答案:

答案 0 :(得分:4)

出于可读性目的清理了查询之后,我有以下内容。

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,
      convert(bigint, round( coalesce(
         case @typecalc
               when 1 then t.valueFromTran
               when 2 then t.netvalue
               when 3 then t.qty
               when 4 then t.valueFromPrice2
               when 5 then t.valueFromPrice3
               when 6 then t.valueFromPrice4
               else 0 
            END,   0.00) * ld.value , 0 ) ),
      @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 (   @tpdim1 is null
           OR ld.dim1 = 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 )
      and (   @tpdim2 is null
           OR ld.dim2 = case when @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 )

此外,我将确保您的loyalty_policy_data表上有一个复合索引...上的索引(loyalty_policy,tdateactive,fdateactive,dbupddate,dim1,dim2)

通过这种方式,您可以限定WHERE过滤条件中使用的所有字段。不要只依赖键的索引...而键加上日期将有助于优化特定的日期范围,而不必返回原始数据页面,但是可以基于INDEX中的值优化查询联接条件

对于临时表#tbl_data,请确保您在(insdate)上有一个索引,因为这是唯一的JOIN基础条件(以防万一您在该表上没有索引)。

评论-

基于

的空值,您对慢速查询与快速查询的评论

@ tpdim1 = NULL 与 '客户'= NULL

固定字符串'CUSTOMER'永远不会为null,因此它永远不必考虑null路径。固定字符串'CUSTOMER'与@customer变量为null或在ld.dim1和ld.dim2分别与null比较的情况下进行比较...也许需要测试的内容应从< / p>

  and (   @tpdim1 is null
               OR ld.dim1 = case @tpdim1
                               when 'STORE' then t.store 
                               when 'BRAND' then t.brand  ... end
     )

  and ld.dim1 = case @tpdim1
                when NULL then ''
                when 'STORE' then t.store 
                when 'BRAND' then t.brand  ... end

与ld.dim2大小写相同/何时。将@ tpdim1(和@ tpdim2)测试的第一个测试值包括“ NULL”。

答案 1 :(得分:2)

回答您的问题:

  

关于查询分析器的方式和原因的清晰且可重复的解释   在这种情况下的行为会有所不同

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

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


Martin Smith在注释中指出您正在使用服务器版本10.0.2531.0,该服务器版本为2008 SP1,并且未启用参数嵌入优化。您需要在该分支的least SP1 CU5上使OPTION (RECOMPILE)正常工作(正如我期望它在下面的说明中工作)。

Erland Sommarskog在下面提到的他的article中也谈到了这一点。他说您至少需要安装SP2。

如果您无法更新服务器,请查看Erland的文章Dynamic Search Conditions in T‑SQL Version for SQL 2005 and Earlier的较旧版本,以了解在没有适当的OPTION (RECOMPILE)时如何处理这种情况。


这是我的原始答案。

我知道您说过您尝试过,但是我仍然会要求您再次检查。查看症状OPTION (RECOMPILE)应该会有所帮助。

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

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
OPTION(RECOMPILE);

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

您的查询类型类似于Dynamic Search Conditions,我强烈建议阅读Erland Sommarskog的文章。

也可以代替

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
(
    @tpdim1 is null
    OR
    (
            ld.dim1 =
            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
    )
)

对于OPTION (RECOMPILE),当@tpdim1的值为CUSTOMER@customer的值为EL0134366时,优化程序应将此语句转换为简单的

and
(
    ld.dim1 = `EL0134366`
)

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

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

答案 2 :(得分:0)

通常来说,使用literal value进行查询比使用proc parameterlocal variable进行查询要快。

使用文字值时,如果Optimizer未启用,Forced Parameterization将为此值制定特殊计划

Optimizer也可以制作Trivial Plan或简单Parameterize Plan,但您的情况并非如此。

使用参数时,优化器将为该参数创建一个计划 称为Parameter Sniffing, 然后重用该计划。

Option Recompile是解决此问题的一种方法:为每个不同的变量值创建计划,以保持“基数估计”。这很短

因此,具有文字值的查询将始终更快。

  

让我先说一下我们尝试过和失败的事情:

     

•通过使用选项(重新编译)和选项避免参数嗅探   (针对(@var UNKOWN)进行优化

     

•此线程。似乎是问题的变量实际上是   本地的而不是proc参数。

您失败是因为您的查询编写得很糟糕(应引起尊敬)。

请勿使用光标。看来您可以避免使用游标

使用变量参数发布完整的proc查询,因为不清楚在@ loop2_loyalty_policy等中获取值的逻辑。这将有助于给出正确的建议“避免游标”。

case when @tpdim1 is null:可以创建此完整的逻辑并将其插入到Temp表本身中,以便可以在join中立即使用新列。希望您能够理解我的想法和语言。

  

1。关于索引假脱机操作的第一个(慢速)查询的估计行数约为9700,而实际行数为300万。

由于优化器估计的基数较高,因此如果加入错误

我不确定,因为我还不是100%理解您的查询,这是否一定会改善您的查询和基数估计。

但是更改加入条件通常会有所帮助,

请仔细阅读,我不确定loyalty_policyt.insdate列中有什么数据。看来您不需要像下面这样的复杂联接。

如果确实需要,则可以像下面一样alter join condition

from loyalty_policy_data ld with (nolock)
 inner join #tbl_data t on ld.loyalty_policy = @loop2_loyalty_policy
 and ld.tdateactive >= @from_rundate and ld.fdateactive <= @to_rundate 
and t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive
                  where t.dbupddate > @loop1_dbupddate 

主要目标是避免出现光标。