重新排列 OR 条件时,SQL Server 创建不同的计划

Sal*_*n A 7 sql-server optimization execution-plan sql-server-2012

我正在审查一个性能不佳的查询,如下所示:

WHERE manymany.Active = -1
  AND manymany.Check1 = -1
  AND manymany.WebsiteID = @P1
  AND CURRENT_TIMESTAMP BETWEEN ISNULL(manymany.FromDate, '1950-01-01') AND ISNULL(manymany.UptoDate, '2050-01-01')
  AND main.Active = -1
  AND main.StatusID = 1
  AND CURRENT_TIMESTAMP BETWEEN main.FromDate AND ISNULL(main.UptoDate, '2050-01-01')
  AND (main.TextCol1 IS NOT NULL OR main.TextCol2 IS NOT NULL)
ORDER BY aux.SortCode
Run Code Online (Sandbox Code Playgroud)

我不小心在这个查询上使用了 SSMS 查询设计器,它重新编写了查询,如下所示:

WHERE manymany.Active = -1
  AND manymany.Check1 = -1
  AND manymany.WebsiteID = @P2
  AND CURRENT_TIMESTAMP BETWEEN ISNULL(manymany.FromDate, '1950-01-01') AND ISNULL(manymany.UptoDate, '2050-01-01')
  AND main.Active = -1
  AND main.StatusID = 1
  AND CURRENT_TIMESTAMP BETWEEN main.FromDate AND ISNULL(main.UptoDate, '2050-01-01')
  AND main.TextCol1 IS NOT NULL

   OR manymany.Active = -1
  AND manymany.Check1 = -1
  AND manymany.WebsiteID = @P2
  AND CURRENT_TIMESTAMP BETWEEN ISNULL(manymany.FromDate, '1950-01-01') AND ISNULL(manymany.UptoDate, '2050-01-01')
  AND main.Active = -1
  AND main.StatusID = 1
  AND CURRENT_TIMESTAMP BETWEEN main.FromDate AND ISNULL(main.UptoDate, '2050-01-01')
  AND main.TextCol2 IS NOT NULL
ORDER BY aux.SortCode
Run Code Online (Sandbox Code Playgroud)

如果您仔细观察,您会注意到它只是OR通过重复所有条件来扩展条件,即它更改a AND (b OR c)(a AND b) OR (a AND c)

结果查询的成本减少了 50%,执行时间减少了 33%。我只是不明白为什么OR在两个查询相同(?)时重新安排条件会改变计划。我可以OR通过复制粘贴条件来自己扩展条件,但我为什么要这样做?

粘贴方案和截图:

执行计划

行数:

main     2718
manymany 188761
aux      19
Run Code Online (Sandbox Code Playgroud)

笔记:

  • TextCol1 和 TextCol2 是text数据类型,不能被索引
  • 有平均。每个网站 id 的 manymany 表中有 170.20 条记录

Ran*_*gen 2

但为什么 SQL Server 不将这两个查询视为一个查询呢?毕竟,a AND (b OR c) = (a AND b) OR (a AND c)?

逻辑上是一样的,也会得到同样的结果。

假设

我的假设是,对于“更快”的计划,优化器不会考虑顶部的某些过滤器语句与OR底部的某些过滤器语句相同。我可能完全没有根据。

获得这些假设的推理基于以下过滤谓词:

Main该过滤谓词使用表与表之间的联接结果manymany在此输入图像描述

请注意,此过滤器中的EXPR1021EXPR1022是根据表上的标量运算符创建的表达式manymany

在此输入图像描述

该过滤器由两部分组成,第一部分为普通过滤(.. AND .. OR .. AND ..) ,第二部分为普通AND过滤

(getdate()>=[Expr1021] 
AND getdate()<=[Expr1022] 
AND getdate()>=[DB1].[dbo].[main].[FromDate] 
AND getdate()<=isnull([DB1].[dbo].[main].[UptoDate],'2050-01-01 00:00:00.000') 
AND [DB1].[dbo].[main].[TextCol1] IS NOT NULL 
OR getdate()>=[Expr1021] 
AND getdate()<=[Expr1022]
 AND getdate()>=[DB1].[dbo].[main].[FromDate] 
 AND getdate()<=isnull([DB1].[dbo].[main].[UptoDate],'2050-01-01 00:00:00.000') 
 AND [DB1].[dbo].[main].[TextCol2] IS NOT NULL) 

 AND (getdate()>=[DB1].[dbo].[main].[FromDate] 
 AND getdate()<=isnull([DB1].[dbo].[main].[UptoDate],'2050-01-01 00:00:00.000') 
 AND [DB1].[dbo].[main].[TextCol1] IS NOT NULL OR getdate()>=[DB1].[dbo].[main].[FromDate] 
 AND getdate()<=isnull([DB1].[dbo].[main].[UptoDate],'2050-01-01 00:00:00.000') 
 AND [DB1].[dbo].[main].[TextCol2] IS NOT NULL)
Run Code Online (Sandbox Code Playgroud)

OR正如您所看到的,该过滤器第一部分 上方和下方的唯一区别是

AND [DB1].[dbo].[main].[TextCol1] IS NOT NULL
Run Code Online (Sandbox Code Playgroud)

VS

AND [DB1].[dbo].[main].[TextCol2] IS NOT NULL
Run Code Online (Sandbox Code Playgroud)

无论如何,第二部分必须为真,因为它们是AND没有任何 's 的谓词OR

导致相同函数的额外计算,我认为这是不需要的。再次,我的猜测是,sql server 进行这些计算的原因是它不知道它们是相同的。

对于 where 子句的某些其他部分,它确实知道这些部分是相同的,例如在主表中, statusid = 1 仅评估一次:

在此输入图像描述

manymany表中,相同的语句被计算两次:

在此输入图像描述

在“慢”计划中,语句不与OR子句添加在一起,这就是为什么优化器生成不同的计划,在表上单独应用过滤谓词(并且没有重复的过滤器)。

在此输入图像描述

在此输入图像描述

假设结束

两个计划的比较

我认为您对“快速”计划的性能很幸运,但是当匹配数据增加时,“快速”计划可能会变得丑陋。这可能取决于您应用过滤器的位置和时间(以及其他因素)

快速计划过滤

在“快速”计划中:由于两个+ ( ) 块的不同组合,sql server 在main表与表连接后应用一些过滤器。在找到与表的所有可能的组合后,对列中的列进行过滤。manymanyORAND ... AND ... AND...maintablemanymany

结果,相同的谓词在manymany表上执行了两次:

在此输入图像描述 对于 之上和之下的谓词OR

main但表上的某些查找谓词并非如此

在此输入图像描述

在此之后,连接发生,并且对于所有可能的组合,再次 针对main和之间的连接结果进行更大的过滤谓词manymany在此输入图像描述

请注意,此过滤器中的EXPR1021EXPR1022是根据表上的标量运算符创建的表达式manymany

在此输入图像描述

该过滤器由两部分组成,第一部分为普通过滤(.. AND .. OR .. AND ..) ,第二部分为普通AND过滤

(getdate()>=[Expr1021] 
AND getdate()<=[Expr1022] 
AND getdate()>=[DB1].[dbo].[main].[FromDate] 
AND getdate()<=isnull([DB1].[dbo].[main].[UptoDate],'2050-01-01 00:00:00.000') 
AND [DB1].[dbo].[main].[TextCol1] IS NOT NULL 
OR getdate()>=[Expr1021] 
AND getdate()<=[Expr1022]
 AND getdate()>=[DB1].[dbo].[main].[FromDate] 
 AND getdate()<=isnull([DB1].[dbo].[main].[UptoDate],'2050-01-01 00:00:00.000') 
 AND [DB1].[dbo].[main].[TextCol2] IS NOT NULL) 

 AND (getdate()>=[DB1].[dbo].[main].[FromDate] 
 AND getdate()<=isnull([DB1].[dbo].[main].[UptoDate],'2050-01-01 00:00:00.000') 
 AND [DB1].[dbo].[main].[TextCol1] IS NOT NULL OR getdate()>=[DB1].[dbo].[main].[FromDate] 
 AND getdate()<=isnull([DB1].[dbo].[main].[UptoDate],'2050-01-01 00:00:00.000') 
 AND [DB1].[dbo].[main].[TextCol2] IS NOT NULL)
Run Code Online (Sandbox Code Playgroud)

OR正如您所看到的,该过滤器第一部分 上方和下方的唯一区别是

AND [DB1].[dbo].[main].[TextCol1] IS NOT NULL
Run Code Online (Sandbox Code Playgroud)

VS

AND [DB1].[dbo].[main].[TextCol2] IS NOT NULL
Run Code Online (Sandbox Code Playgroud)

无论如何,第二部分必须为真,因为它们是AND没有任何 's 的谓词OR

导致我认为不需要的额外计算。

慢计划过滤

在“慢”计划中:sql server 根据AND (TextCol1 IS NOT NULL OR TextCol2 IS NOT NULL) 部分将过滤器直接应用于主表,然后与manymany表连接以过滤掉其余部分以达到 15 行。

Main表过滤器

在此输入图像描述

在此输入图像描述

manymany表过滤器

在此输入图像描述


其他一些有时重叠的信息:

较慢的计划

当我们查看较慢的计划时,将聚集索引 PK_main 用于计算标量、过滤器和嵌套循环运算符:

在此输入图像描述

当我们将其与要返回的估计行数进行比较时,我们看到了差异: 在此输入图像描述

估计扫描谓词将返回 93 行:

在此输入图像描述

实际上比预期的1947 行少了大约 20 倍。

之后,计算标量或此语句:

 , CASE WHEN TextCol1 IS NOT NULL OR TextCol2 IS NOT NULL THEN -1 ELSE 0 END AS MoreFlag
 , CASE WHEN Stars BETWEEN 1 AND 5 THEN Stars END AS Rating
Run Code Online (Sandbox Code Playgroud)

对这 1947 行进行评估。

然后过滤运算符 ( main.TextCol1 IS NOT NULL OR main.TextCol2 IS NOT NULL) 将其减少到 1374 行。

之后,将这 1374 行连接到dbo.manymany表中以获取返回的 15 行。

更快的计划

更快的计划是使用 NC 索引:CVR_main_4the dbo.Main表上, 在此输入图像描述

它使用查找谓词进行过滤,将 27 行返回给nested loopsJoin 运算符,再次与表连接dbo.manymany

并且实际返回的行数甚至低于估计的行数

在此输入图像描述

实际 27 行,估计 152 行

过滤

一个很大的区别是过滤发生的位置,在“较慢”计划中,这是直接在桌面上完成的dbo.Main

使用谓词:TextCol1 IS NOT NULL OR TextCol2 IS NOT NULL

在此输入图像描述

并将此过滤器应用于 1943 行。

其他过滤直接在dbo.manymany桌面上进行

在此输入图像描述 (寻求)谓词dbo.manymany

而另一个,在“更快”的计划中,在连接 from toOR之后被过滤,并在 27 行上产生一个更大的过滤器。dbo.Maindbo.manymany

在此输入图像描述

OR更大的过滤器, 27 行有多个。

另一个区别是键查找运算符:

在此输入图像描述

它从聚集索引中获取 10 个额外列,但只需对 27 行执行此操作。

在此输入图像描述

在此输入图像描述

优化器选择“较慢”计划的另一个原因可能是因为优化器认为不查找其他列会更好。


快速计划是更快,还是总是“更快”?

我确实认为,如果通过过滤器的数据增加,“慢”计划会更好。不仅是由于键查找,还因为计划中更靠后的更大的过滤器运算符。

如果发生这种情况,就在索引旁边。您可以通过使用语句将查询拆分为多个部分来改进过滤UNION

就像这样:

SELECT main.MainID, Title, Column1, Column2, Column7, Column4, Column6, Column3, Column5
     , CASE WHEN TextCol1 IS NOT NULL OR TextCol2 IS NOT NULL THEN -1 ELSE 0 END AS MoreFlag
     , CASE WHEN Stars BETWEEN 1 AND 5 THEN Stars END AS Rating
FROM manymany
INNER JOIN main ON manymany.MainID = main.MainID
LEFT JOIN aux ON manymany.AuxID = aux.AuxID
WHERE manymany.WebsiteID = @P1
  AND manymany.Check1 = -1
  AND manymany.Active = -1
  AND CURRENT_TIMESTAMP BETWEEN ISNULL(manymany.FromDate, '1950-01-01') AND ISNULL(manymany.UptoDate, '2050-01-01')
  AND main.Active = -1
  AND main.StatusID = 1
  AND CURRENT_TIMESTAMP BETWEEN main.FromDate AND ISNULL(main.UptoDate, '2050-01-01')
  AND TextCol1 IS NOT NULL

 UNION 

 SELECT main.MainID, Title, Column1, Column2, Column7, Column4, Column6, Column3, Column5
     , CASE WHEN TextCol1 IS NOT NULL OR TextCol2 IS NOT NULL THEN -1 ELSE 0 END AS MoreFlag
     , CASE WHEN Stars BETWEEN 1 AND 5 THEN Stars END AS Rating
FROM manymany
INNER JOIN main ON manymany.MainID = main.MainID
LEFT JOIN aux ON manymany.AuxID = aux.AuxID
WHERE manymany.WebsiteID = @P1
  AND manymany.Check1 = -1
  AND manymany.Active = -1
  AND CURRENT_TIMESTAMP BETWEEN ISNULL(manymany.FromDate, '1950-01-01') AND ISNULL(manymany.UptoDate, '2050-01-01')
  AND main.Active = -1
  AND main.StatusID = 1
  AND CURRENT_TIMESTAMP BETWEEN main.FromDate AND ISNULL(main.UptoDate, '2050-01-01')
  AND TextCol2 IS NOT NULL
ORDER BY SortCode;
Run Code Online (Sandbox Code Playgroud)

  • @RandiVertongen 在这里你解释了为什么不同的计划有不同的性能以及哪一个应该更好,但我OP的问题是为什么编写两个表达式*完全等效*会影响优化器的计划。SQL(理论上)是一种声明性语言(非命令式),因此我们告诉引擎我们想要什么,而不是它应该如何做。对于这个例子,我们似乎可以影响如何。 (2认同)