相同的查询在进程外时运行得更快
Same query runs faster when out of proc
我们有一个特定的查询,运行在过程中时速度要慢得多。我必须在这里补充一点,它包含在一个两级光标中。但是,两个游标都有一行的迭代结果集。
让我首先说明我们尝试过但失败的事情:
- 通过使用选项(重新编译)和选项(优化(@var UNKNOWN)来避免参数嗅探
- This thread。似乎是问题的变量实际上是本地变量而不是 proc 参数。
这是从 proc/cursors.
内部获取的查询
select @tpdim1 = dim1, @tpdim2 = dim2, @typecalc = typecalc
from loyalty_policy where code=@loop2_loyalty_policy
注意:@loop2_loyalty_policy是从内部游标的结果中取出的var,只有一个值。 code
是PK给loyalty_policy
table。因此,@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
为returnsthis plan.
在尝试调试的过程中,我们将query隔离成下面的形式(这里也可以看到table#a是怎么做的,和前面的#[=93数据一模一样=]):
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是执行计划。这 运行 快了很多。
为什么会有如此巨大的差异?由于我对执行分析的了解有限,我注意到
- 第一个(慢速)查询,在
index spool
操作中,估计行数约为 9700,但实际行数为 300 万。
- 第二个查询使用了许多并行操作
- 我在第二个查询中看到的唯一 "real" 区别是 @tpdim1 和 @tpdim2 值的手动替换值。 果然,当我们进入第一个查询的 proc 代码,并用它们应该得到的单个值替换 @tpdim1 和 @tpdim2 时,它 运行 和第二个查询一样快.
能否请您解释一下这种差异并提出一些修复程序的建议?
编辑:正如 Laughing Vergil 所建议的那样,我用之前声明的变量替换了第二个查询中的文字,并且再次 运行 慢!
编辑 2:我通过进一步研究获得了一些额外信息。
首先,我已将问题隔离到这一行:
case when @tpdim1 is null then ''
<-- 这使用慢计划
case when 'CUSTOMER' is null then ''
<-- 这使用快速计划
在临时查询中确实如此,无需为 spcs and/or 游标烦恼。
即使我将代码更改为推荐的动态 where 结构,这仍然会发生。
我还没有创建任何样本数据,但重要的信息(如计划中所示)是如果我们仅按 loyalty_policy = @loop2_loyalty_policy
过滤,loyalty_policy_data
有大约 720k 行。但是,如果我们评估 @tpdim1 条件(本质上是 dim1=N'EL0134366'),则返回的行仅为 4.
那么,计划的不同之处在于何时根据日期检查条件评估此条件。
在快速计划中,它首先被评估 - 在寻找忠诚度政策值的索引时,它添加了一个(非寻找)谓词。虽然此谓词不在索引中,但返回的行数为 4,所有其他运算符的大小为 "logical"。
相比之下,缓慢的计划痛苦地忽略了这个谓词,直到为时已晚。如果我计算正确,它会在 loyalty_policy_data 上创建一个嵌套循环作为外部 table(这太疯狂了)。它将所需的列作为外部引用传递。对于每个这样的元组,索引假脱机扫描 #table(~1k 行)并找到大约 250 个结果,并将其传递给 finally 进行 tpdim1 过滤的过滤器。因此,250*700k 行被传递给过滤器运算符。
所以现在我想我知道会发生什么。但我不明白为什么。
回答您的问题:
A clear and reproducible explanation of how and why the query analyzer
behaves differently in those cases
查询优化器在这些情况下的行为不同,因为带有变量的计划必须对 任何 可能的参数未来值有效,因此优化器会生成一个复杂的通用计划,该计划会产生即使参数为 NULL,结果也正确。
带有文字(不是变量)的计划通常更有效,因为优化器可以在计划编译阶段大大简化您的 CASE
逻辑。优化器有更好的机会选择最佳计划形状,因为当查询更简单且过滤器具有已知值时,优化器更容易考虑有关索引和基数估计的可用信息。
Martin Smith pointed out in the comment that you are using the server version 10.0.2531.0, which is 2008 SP1 and which does not have the parameter embedding optimization enabled. You would need at 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
)
)
With OPTION (RECOMPILE)
当 @tpdim1
的值为 CUSTOMER
且 @customer
的值为 EL0134366
时,优化器应将此语句转换为简单的
and
(
ld.dim1 = `EL0134366`
)
然后它将能够使用合适的索引或更准确地估计行数,并对计划形状做出更好的决定。使用此选项,计划将仅对参数的此特定值有效。
请注意,option (optimize for UNKNOWN)
在这里无能为力。 optimize for UNKNOWN
必须生成对任何可能的参数值都有效的通用计划。
出于可读性目的清理查询后,我有以下内容。
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 table... 上有一个复合索引...索引 ( loyalty_policy, tdateactive, fdateactive, dbupddate, dim1, dim2 )
这样您就可以限定 WHERE 过滤条件中使用的所有字段。不要只依赖键的索引...但是键加上日期将有助于优化特定的日期范围,而无需返回原始数据页面,但可以根据 INDEX 中的值优化查询 JOIN 条件.
至于你的临时 table #tbl_data,确保你在 ( insdate ) 上有一个索引,因为这是唯一的 JOIN 基础标准(如果你还没有那个索引table).
评论--
根据您对基于
的空值的慢速查询与快速查询的评论
@tpdim1 = NULL
对比
'CUSTOMER' = NULL
固定字符串 'CUSTOMER' 永远不会为空,因此它永远不必在空路径下考虑它。修复了字符串 'CUSTOMER' 与 @customer 变量为 null 或在 ld.dim1 和 ld.dim2 的 case/when 中分别与 null 进行比较...也许需要测试的内容应该更改自
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 相同 case/when。包括 "NULL" 作为 @tpdim1(和 @tpdim2)测试的第一个测试值。
一般来说,使用 literal value
的查询比使用 proc parameter
或 local variable
的查询更快。
当使用文字值时,如果 Forced Parameterization
未打开
,Optimizer
将只为该值制定特别计划
Optimizer 也可以使 Trivial Plan
或 Simple Parameterize Plan
但在你的情况下这不是真的。
当您使用参数时,优化器会为该参数创建一个计划
这叫做 Parameter Sniffing
,
然后重用该计划。
Option Recompile
是克服这个问题的一种方法:为每个不同的变量值创建计划,以保持“基数估计”。这很短
因此使用文字值的查询总是更快。
Let me first state things that we tried and failed:
•Avoiding parameter sniffing by using option (recompile) and option
(optiimize for (@var UNKOWN)
•This thread. The variables that seems to be the problem are actually
local ones and not proc parameters.
你失败了,因为你的查询写得非常糟糕(恕我直言)。
不使用游标。在你的情况下似乎可以避免光标
Post 使用可变参数完成 proc 查询,因为在 @loop2_loyalty_policy 等中获取值的逻辑不是 clear.This 将有助于给出正确的建议 "to avoid cursor".
case when @tpdim1 is null
: 这个完整的逻辑可以在 Temp table 本身中创建和插入,这样新列就可以直接在 join.Hope 中使用你可以理解我的想法和语言。
1.The first (slow) query, on the index spool operation, has an estimated rows of ~9700 but actual rows of 3 million.
由于 optmizer 的高基数估计,如果连接错误
我不确定这是否一定会改进您的查询和基数估计,因为我还没有 100% 理解您的查询。
但是改变连接条件通常有帮助,
仔细阅读这里,我不确定loyalty_policy
和t.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
主要目标是避免游标。
我们有一个特定的查询,运行在过程中时速度要慢得多。我必须在这里补充一点,它包含在一个两级光标中。但是,两个游标都有一行的迭代结果集。
让我首先说明我们尝试过但失败的事情:
- 通过使用选项(重新编译)和选项(优化(@var UNKNOWN)来避免参数嗅探
- This thread。似乎是问题的变量实际上是本地变量而不是 proc 参数。
这是从 proc/cursors.
内部获取的查询 select @tpdim1 = dim1, @tpdim2 = dim2, @typecalc = typecalc
from loyalty_policy where code=@loop2_loyalty_policy
注意:@loop2_loyalty_policy是从内部游标的结果中取出的var,只有一个值。 code
是PK给loyalty_policy
table。因此,@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
为returnsthis plan.
在尝试调试的过程中,我们将query隔离成下面的形式(这里也可以看到table#a是怎么做的,和前面的#[=93数据一模一样=]):
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是执行计划。这 运行 快了很多。
为什么会有如此巨大的差异?由于我对执行分析的了解有限,我注意到
- 第一个(慢速)查询,在
index spool
操作中,估计行数约为 9700,但实际行数为 300 万。 - 第二个查询使用了许多并行操作
- 我在第二个查询中看到的唯一 "real" 区别是 @tpdim1 和 @tpdim2 值的手动替换值。 果然,当我们进入第一个查询的 proc 代码,并用它们应该得到的单个值替换 @tpdim1 和 @tpdim2 时,它 运行 和第二个查询一样快.
能否请您解释一下这种差异并提出一些修复程序的建议?
编辑:正如 Laughing Vergil 所建议的那样,我用之前声明的变量替换了第二个查询中的文字,并且再次 运行 慢!
编辑 2:我通过进一步研究获得了一些额外信息。
首先,我已将问题隔离到这一行:
case when @tpdim1 is null then ''
<-- 这使用慢计划
case when 'CUSTOMER' is null then ''
<-- 这使用快速计划
在临时查询中确实如此,无需为 spcs and/or 游标烦恼。
即使我将代码更改为推荐的动态 where 结构,这仍然会发生。
我还没有创建任何样本数据,但重要的信息(如计划中所示)是如果我们仅按 loyalty_policy = @loop2_loyalty_policy
过滤,loyalty_policy_data
有大约 720k 行。但是,如果我们评估 @tpdim1 条件(本质上是 dim1=N'EL0134366'),则返回的行仅为 4.
那么,计划的不同之处在于何时根据日期检查条件评估此条件。
在快速计划中,它首先被评估 - 在寻找忠诚度政策值的索引时,它添加了一个(非寻找)谓词。虽然此谓词不在索引中,但返回的行数为 4,所有其他运算符的大小为 "logical"。
相比之下,缓慢的计划痛苦地忽略了这个谓词,直到为时已晚。如果我计算正确,它会在 loyalty_policy_data 上创建一个嵌套循环作为外部 table(这太疯狂了)。它将所需的列作为外部引用传递。对于每个这样的元组,索引假脱机扫描 #table(~1k 行)并找到大约 250 个结果,并将其传递给 finally 进行 tpdim1 过滤的过滤器。因此,250*700k 行被传递给过滤器运算符。
所以现在我想我知道会发生什么。但我不明白为什么。
回答您的问题:
A clear and reproducible explanation of how and why the query analyzer behaves differently in those cases
查询优化器在这些情况下的行为不同,因为带有变量的计划必须对 任何 可能的参数未来值有效,因此优化器会生成一个复杂的通用计划,该计划会产生即使参数为 NULL,结果也正确。
带有文字(不是变量)的计划通常更有效,因为优化器可以在计划编译阶段大大简化您的 CASE
逻辑。优化器有更好的机会选择最佳计划形状,因为当查询更简单且过滤器具有已知值时,优化器更容易考虑有关索引和基数估计的可用信息。
Martin Smith pointed out in the comment that you are using the server version 10.0.2531.0, which is 2008 SP1 and which does not have the parameter embedding optimization enabled. You would need at 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
)
)
With OPTION (RECOMPILE)
当 @tpdim1
的值为 CUSTOMER
且 @customer
的值为 EL0134366
时,优化器应将此语句转换为简单的
and
(
ld.dim1 = `EL0134366`
)
然后它将能够使用合适的索引或更准确地估计行数,并对计划形状做出更好的决定。使用此选项,计划将仅对参数的此特定值有效。
请注意,option (optimize for UNKNOWN)
在这里无能为力。 optimize for UNKNOWN
必须生成对任何可能的参数值都有效的通用计划。
出于可读性目的清理查询后,我有以下内容。
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 table... 上有一个复合索引...索引 ( loyalty_policy, tdateactive, fdateactive, dbupddate, dim1, dim2 )
这样您就可以限定 WHERE 过滤条件中使用的所有字段。不要只依赖键的索引...但是键加上日期将有助于优化特定的日期范围,而无需返回原始数据页面,但可以根据 INDEX 中的值优化查询 JOIN 条件.
至于你的临时 table #tbl_data,确保你在 ( insdate ) 上有一个索引,因为这是唯一的 JOIN 基础标准(如果你还没有那个索引table).
评论--
根据您对基于
的空值的慢速查询与快速查询的评论@tpdim1 = NULL 对比 'CUSTOMER' = NULL
固定字符串 'CUSTOMER' 永远不会为空,因此它永远不必在空路径下考虑它。修复了字符串 'CUSTOMER' 与 @customer 变量为 null 或在 ld.dim1 和 ld.dim2 的 case/when 中分别与 null 进行比较...也许需要测试的内容应该更改自
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 相同 case/when。包括 "NULL" 作为 @tpdim1(和 @tpdim2)测试的第一个测试值。
一般来说,使用 literal value
的查询比使用 proc parameter
或 local variable
的查询更快。
当使用文字值时,如果 Forced Parameterization
未打开
Optimizer
将只为该值制定特别计划
Optimizer 也可以使 Trivial Plan
或 Simple Parameterize Plan
但在你的情况下这不是真的。
当您使用参数时,优化器会为该参数创建一个计划
这叫做 Parameter Sniffing
,
然后重用该计划。
Option Recompile
是克服这个问题的一种方法:为每个不同的变量值创建计划,以保持“基数估计”。这很短
因此使用文字值的查询总是更快。
Let me first state things that we tried and failed:
•Avoiding parameter sniffing by using option (recompile) and option (optiimize for (@var UNKOWN)
•This thread. The variables that seems to be the problem are actually local ones and not proc parameters.
你失败了,因为你的查询写得非常糟糕(恕我直言)。
不使用游标。在你的情况下似乎可以避免光标
Post 使用可变参数完成 proc 查询,因为在 @loop2_loyalty_policy 等中获取值的逻辑不是 clear.This 将有助于给出正确的建议 "to avoid cursor".
case when @tpdim1 is null
: 这个完整的逻辑可以在 Temp table 本身中创建和插入,这样新列就可以直接在 join.Hope 中使用你可以理解我的想法和语言。
1.The first (slow) query, on the index spool operation, has an estimated rows of ~9700 but actual rows of 3 million.
由于 optmizer 的高基数估计,如果连接错误
我不确定这是否一定会改进您的查询和基数估计,因为我还没有 100% 理解您的查询。
但是改变连接条件通常有帮助,
仔细阅读这里,我不确定loyalty_policy
和t.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
主要目标是避免游标。