需要帮助了解一些执行计划

时间:2018-06-07 09:07:39

标签: sql sql-server tsql

假设一个简单的表定义为:

CREATE TABLE Table1
(
[ID] [bigint] NOT NULL IDENTITY(1, 1) NOT FOR REPLICATION,
[State] [tinyint] NOT NULL DEFAULT ((0))
)
ALTER TABLE Table1 ADD CONSTRAINT [PK_Table1] PRIMARY KEY CLUSTERED  ([ID])
CREATE NONCLUSTERED INDEX [IX_NC_F_Media_StateNotDeleted] ON Table1 ([State]) WHERE ([State]<>(2))
CREATE NONCLUSTERED INDEX [IX_NC_F_Media_State] ON Table1 ([State]) WHERE ([State]=(0))

插入值如下:

250000 rows with State = 0
1000 rows with State = 5

以下查询及其各自的执行计划:

declare @mID int = 400000;
select State 
from Table1 
where (ID = @mID and State in (0, 1, 4, 5))
   or (@mID IS NULL and State in (0, 4))

enter image description here

考虑到ID不为空,因此@mid IS NULLID = @mID互斥,我将查询重写为:

declare @mID int = 400000;
if @mID is null
begin
  select State 
  from Table1 
  where State in (0, 4)
end
else
begin
  select State 
  from Table1 
  where ID=@mID and State in (0, 1, 4, 5)
end

enter image description here

问题:

  1. 为什么这两种情况之间的执行计划存在差异?为什么非聚集索引仅用于第二种情况?
  2. 虽然第二种情况对@mID为空时的情况执行搜索与扫描,但它确实有任何区别吗?工具提示表明性能命中率几乎相同,我猜这是因为数据主要是State = 0行。

3 个答案:

答案 0 :(得分:2)

您的查询未使用第一个查询中的索引,因为该变量已被用作条件的一部分(@mID IS NULL和State in(0,4)),在这种情况下@mID IS为NULL,因为这是不是列不能包含在索引中,这就是sql无法激活索引的原因。在第二个查询中,如果要删除此问题并且已激活索引。

在if的第二部分中,因为你有聚簇索引,sql更快找到这个特定的记录,然后评估状态是否有任何这些值来显示结果。

在问题范围之外的其他问题,经过一些测试我发现了一些有趣的东西。当状态的值变得更加平衡时,让我们说每个可能的选项形式1到5都有值,执行计划更改,第一个查询变得和使用if的选项一样好。

declare @mID int = 400000;
select State 
from Table1 
where (ID = @mID and State in (0, 1, 4, 5))
   or (@mID IS NULL and State in (0, 4))

enter image description here

答案 1 :(得分:2)

通常,当优化程序为查询生成计划时,此计划必须对任何可能的参数值有效。通常,计划被缓存,并且当您再次运行相同的查询时不会重新生成计划,因此即使您使用不同的参数值重新运行查询,它也必须保持有效(产生正确的结果)。

因此,第一个查询的计划必须具有适用于@mID的任何值的形状,包括NULL和非NULL。

索引IX_NC_F_Media_StateNotDeleted可用于查找OR表达式的两个部分的值,但是任何一个优化器都不够智能,无法构建执行两次查找此索引的计划,然后将结果联合起来或者,优化者决定这样的计划会更加昂贵。

因此,要么优化者无法看到 here @mid IS NULLID = @mID互斥,或者它决定替代品会更贵。

使用显式IF的第二个查询使得优化者的选择变得明显。

这种类型的查询称为“catch-all”或“sink”查询。我建议阅读Erland Sommarskog撰写的优秀文章Dynamic Search Conditions in T‑SQL

在许多情况下,最好在第一个查询中添加OPTION (RECOMPILE),如下所示:

declare @mID int = 400000;
select State 
from Table1 
where (ID = @mID and State in (0, 1, 4, 5))
   or (@mID IS NULL and State in (0, 4))
OPTION(RECOMPILE);

SET @mID = NULL;
select State 
from Table1 
where (ID = @mID and State in (0, 1, 4, 5))
   or (@mID IS NULL and State in (0, 4))
OPTION(RECOMPILE);

尝试运行这些查询并检查其实际执行计划。您应该看到计划的形状根据执行时参数的实际值而变化。

使用OPTION(RECOMPILE)优化器知道生成的计划不会像往常一样缓存,因此它接受参数的实际值并将它们作为常量内联到查询中。一旦它们成为常量, here ,优化器就能看到NULL IS NULL始终为真(或400000 IS NULL始终为false)并折叠/简化逻辑表达式。此外,优化者能够在每种情况下选择最合适的指标。

  

虽然第二种情况执行搜索与扫描的情况   @mID为null,它真的有什么区别吗?工具提示   表明性能命中几乎是相同的,我是   猜测这是因为数据主要是State = 0行。

在这种情况下,寻找索引与扫描整个表几乎相同。它们的大小(以页为单位)是相同的。如果你的表有很多行State=2,那么过滤后的索引会更有效率,因为它包含的页面数量比主表少。

答案 2 :(得分:1)

以下是我对此问题的理解: 在第一种情况下,SQL Server必须提出一个查询计划,该计划可以满足是否已经传递了@mid值。

第一个查询中的两个条件将受益于不同的索引,但是逻辑上需要加入结果集,因为它是OR条件。因此,它决定了一个能够在聚集索引的单次扫描中满足这两个条件的计划。

在第二种情况下,问题被消除,因此SQL服务器可以在单独的查询上使用不同的索引