你能解释一下这个执行计划吗?

And*_*ykh 20 sql-server execution-plan sql-server-2008-r2

当我遇到这个东西时,我正在研究其他东西。我正在生成包含一些数据的测试表并运行不同的查询,以了解编写查询的不同方式如何影响执行计划。这是我用来生成随机测试数据的脚本:

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID('t') AND type in (N'U'))
DROP TABLE t
GO

CREATE TABLE t 
(
 c1 int IDENTITY(1,1) NOT NULL 
,c2 int NULL
) 
GO

insert into t
select top 1000000 a from
(select t1.number*2048 + t2.number a, newid() b
from [master]..spt_values t1 
cross join  [master]..spt_values t2
where t1.[type] = 'P' and t2.[type] = 'P') a
order by b
GO

update t set c2 = null
where c2 < 2048 * 2048 / 10
GO


CREATE CLUSTERED INDEX pk ON [t] (c1)
GO

CREATE NONCLUSTERED INDEX i ON t (c2)
GO
Run Code Online (Sandbox Code Playgroud)

现在,根据这些数据,我调用了以下查询:

select * 
from t 
where 
      c2 < 1048576 
   or c2 is null
;
Run Code Online (Sandbox Code Playgroud)

令我惊讶的是,为这个查询生成的执行计划是这个. (抱歉,外部链接太大,无法放在这里)。

有人可以向我解释所有这些“常量扫描”和“计算标量”是怎么回事吗?发生了什么?

计划

  |--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1010], [Expr1011], [Expr1012]))
       |--Merge Interval
       |    |--Sort(TOP 2, ORDER BY:([Expr1013] DESC, [Expr1014] ASC, [Expr1010] ASC, [Expr1015] DESC))
       |         |--Compute Scalar(DEFINE:([Expr1013]=((4)&[Expr1012]) = (4) AND NULL = [Expr1010], [Expr1014]=(4)&[Expr1012], [Expr1015]=(16)&[Expr1012]))
       |              |--Concatenation
       |                   |--Compute Scalar(DEFINE:([Expr1005]=NULL, [Expr1006]=NULL, [Expr1004]=(60)))
       |                   |    |--Constant Scan
       |                   |--Compute Scalar(DEFINE:([Expr1008]=NULL, [Expr1009]=(1048576), [Expr1007]=(10)))
       |                        |--Constant Scan
       |--Index Seek(OBJECT:([t].[i]), SEEK:([t].[c2] > [Expr1010] AND [t].[c2] < [Expr1011]) ORDERED FORWARD)
Run Code Online (Sandbox Code Playgroud)

Mar*_*ith 28

常量扫描每个都会产生一个没有列的内存行。顶部计算标量输出单行 3 列

Expr1005    Expr1006    Expr1004
----------- ----------- -----------
NULL        NULL        60
Run Code Online (Sandbox Code Playgroud)

底部计算标量输出单行 3 列

Expr1008    Expr1009    Expr1007
----------- ----------- -----------
NULL        1048576        10
Run Code Online (Sandbox Code Playgroud)

连接运算符将这 2 行联合在一起并输出 3 列,但它们现在已重命名

Expr1010    Expr1011    Expr1012
----------- ----------- -----------
NULL        NULL        60
NULL        1048576     10
Run Code Online (Sandbox Code Playgroud)

Expr1012列是一组内部使用的标志,用于定义存储引擎的某些查找属性

下一个计算标量沿输出 2 行

Expr1010    Expr1011    Expr1012    Expr1013    Expr1014    Expr1015
----------- ----------- ----------- ----------- ----------- -----------
NULL        NULL        60          True        4           16            
NULL        1048576     10          False       0           0      
Run Code Online (Sandbox Code Playgroud)

最后三列定义如下,仅用于在呈现给 Merge Interval Operator 之前进行排序

[Expr1013] = Scalar Operator(((4)&[Expr1012]) = (4) AND NULL = [Expr1010]), 
[Expr1014] = Scalar Operator((4)&[Expr1012]), 
[Expr1015] = Scalar Operator((16)&[Expr1012])
Run Code Online (Sandbox Code Playgroud)

Expr1014Expr1015只是测试是否一定位上的标志。 Expr1013如果位 for4Expr1010is ,似乎返回一个布尔列 true NULL

通过在查询中尝试其他比较运算符,我得到了这些结果

+----------+----------+----------+-------------+----+----+---+---+---+---+
| Operator | Expr1010 | Expr1011 | Flags (Dec) |       Flags (Bin)       |
|          |          |          |             | 32 | 16 | 8 | 4 | 2 | 1 |
+----------+----------+----------+-------------+----+----+---+---+---+---+
| >        | 1048576  | NULL     |           6 |  0 |  0 | 0 | 1 | 1 | 0 |
| >=       | 1048576  | NULL     |          22 |  0 |  1 | 0 | 1 | 1 | 0 |
| <=       | NULL     | 1048576  |          42 |  1 |  0 | 1 | 0 | 1 | 0 |
| <        | NULL     | 1048576  |          10 |  0 |  0 | 1 | 0 | 1 | 0 |
| =        | 1048576  | 1048576  |          62 |  1 |  1 | 1 | 1 | 1 | 0 |
| IS NULL  | NULL     | NULL     |          60 |  1 |  1 | 1 | 1 | 0 | 0 |
+----------+----------+----------+-------------+----+----+---+---+---+---+
Run Code Online (Sandbox Code Playgroud)

从中我推断出第 4 位表示“具有范围开始”(而不是无界),第 16 位表示范围的开始包括在内。

这个 6 列结果集是从SORT按 排序的运算符发出的 Expr1013 DESC, Expr1014 ASC, Expr1010 ASC, Expr1015 DESC。假设True1False0先前表示的结果集已按该顺序表示。

基于我之前的假设,这种类型的净效果是按以下顺序呈现合并间隔的范围

 ORDER BY 
          HasStartOfRangeAndItIsNullFirst,
          HasUnboundedStartOfRangeFirst,
          StartOfRange,
          StartOfRangeIsInclusiveFirst
Run Code Online (Sandbox Code Playgroud)

合并区间运算符输出 2 行

Expr1010    Expr1011    Expr1012
----------- ----------- -----------
NULL        NULL        60
NULL        1048576     10
Run Code Online (Sandbox Code Playgroud)

对于发出的每一行,执行范围搜索

Seek Keys[1]: Start:[dbo].[t].c2 > Scalar Operator([Expr1010]), 
               End: [dbo].[t].c2 < Scalar Operator([Expr1011])
Run Code Online (Sandbox Code Playgroud)

所以看起来好像执行了两次搜索。一明显> NULL AND < NULL> NULL AND < 1048576。然而,传入的标志似乎分别将其修改为IS NULL< 1048576。希望@sqlkiwi可以澄清这一点并纠正任何不准确之处!

如果您将查询稍微更改为

select *
from t 
where 
      c2 > 1048576 
   or c2 = 0
;
Run Code Online (Sandbox Code Playgroud)

然后,使用具有多个搜索谓词的索引搜索,该计划看起来要简单得多。

该计划显示 Seek Keys

Start: c2 >= 0, End: c2 <= 0, 
Start: c2 > 1048576
Run Code Online (Sandbox Code Playgroud)

SQLKiwi 在之前链接的博客文章的评论中解释了为什么这个更简单的计划不能用于 OP 中的案例。

索引查找与多个谓词不能混合不同类型的比较谓词(即的IsEq在OP的情况下)。这只是产品的当前限制(并且大概是为什么在最后一个查询c2 = 0中执行相等性测试的原因>=<=而不仅仅是使用查询c2 = 0 OR c2 = 1048576.

  • @MartinSmith 60 确实用于与 NULL 进行比较。范围边界表达式使用 NULL 来表示两端的“无界”。查找始终是排他的,即查找 Start: &gt; Expr &amp; End: &lt; Expr 而不是包含使用 &gt;= 和 &lt;=。感谢您的博客评论,我会在早上发布一个答案或更长的评论作为回复(现在做正义太晚了)。 (2认同)

Gra*_*hey 13

持续扫描是 SQL Server 创建一个存储桶的一种方式,它将在执行计划中稍后放置一些东西。我在这里发布了更详尽的解释。要了解持续扫描的目的,您必须进一步查看计划。在这种情况下,计算标量运算符用于填充由常量扫描创建的空间。

Compute Scalar 运算符加载了 NULL 和值 1045876,因此它们显然将与 Loop Join 一起使用以过滤数据。

真正酷的部分是这个计划是微不足道的。这意味着它经历了一个最小的优化过程。所有操作都导致合并间隔。这用于为索引查找创建一组最小的比较运算符(此处有详细说明)。

整个想法是摆脱重叠的值,以便它可以以最少的次数提取数据。尽管它仍在使用循环操作,但您会注意到循环只执行一次,这意味着它实际上是一次扫描。

附录:最后一句话是关闭的。有两次寻求。我误读了计划。其余的概念是相同的,目标,最小传球,是相同的。