【问题标题】:Need help understanding some execution plans需要帮助了解一些执行计划
【发布时间】:2018-06-18 23:01:01
【问题描述】:

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

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))

考虑到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

问题:

  1. 为什么这两种情况的执行计划不同?为什么非聚集索引只用于第二种情况?
  2. 尽管当@mID 为空时,第二种情况会执行查找和扫描,但它真的有什么区别吗?工具提示表明性能影响几乎相同,我猜这是因为数据主要是 State = 0 行。

【问题讨论】:

  • 执行计划之间存在差异,因为第一个查询比第二个查询具有更多过滤条件(在 if else 块内)。 SQL 必须创建执行计划,该计划应该在您下次执行相同的查询时起作用(可能是下次您将在参数中传递 NULL)。
  • 如果你执行以上两种情况(第一种@mID不为空,第二种@mID为空),那么你会在SQL缓存中找到三个执行计划(三个执行计划selects您的案例)。
  • @BhatiaAshish:我知道由于 if 语句在第二种情况下存在差异,但我想知道为什么非聚集索引仅在第二种情况下被激活而不是在第一个。
  • SQL Server 仅对此查询使用非聚集索引 - select State from Table1 where State in (0, 4),因为只有一个 NC 索引包含 (0,4) 中的状态数据。

标签: sql sql-server tsql


【解决方案1】:

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

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

索引IX_NC_F_Media_StateNotDeleted 可用于查找OR 表达式的两个部分的值,但要么优化器不够聪明,无法构建一个计划来执行此索引的两次查找然后合并结果,要么优化器决定这样的计划会更昂贵。

因此,要么优化器无法看到 此处 @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 始终为假)并折叠/简化逻辑表达式。此外,优化器能够在每种情况下选择最合适的索引。


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

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

【讨论】:

  • 这是最彻底的答案,似乎与我的观察一致。
【解决方案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))

【讨论】:

    【解决方案3】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-04
      • 1970-01-01
      • 1970-01-01
      • 2017-06-19
      • 2016-05-03
      • 1970-01-01
      相关资源
      最近更新 更多