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


20

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

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

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

select * 
from t 
where 
      c2 < 1048576 
   or c2 is null
;

令我惊讶的是,为该查询生成的执行计划是this。(很抱歉,外部链接太大,无法在此处容纳)。

有人可以向我解释所有这些“ 持续扫描 ”和“ 计算标量 ”如何吗?发生了什么?

计划

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

Answers:


29

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

Expr1005    Expr1006    Expr1004
----------- ----------- -----------
NULL        NULL        60

底部计算标量输出三列的单行

Expr1008    Expr1009    Expr1007
----------- ----------- -----------
NULL        1048576        10

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

Expr1010    Expr1011    Expr1012
----------- ----------- -----------
NULL        NULL        60
NULL        1048576     10

Expr1012列是一组标志,这些标志在内部用于定义Storage Engine的某些查找属性

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

Expr1010    Expr1011    Expr1012    Expr1013    Expr1014    Expr1015
----------- ----------- ----------- ----------- ----------- -----------
NULL        NULL        60          True        4           16            
NULL        1048576     10          False       0           0      

最后三列定义如下,仅用于排序目的,然后再提供给“合并间隔”运算符

[Expr1013] = Scalar Operator(((4)&[Expr1012]) = (4) AND NULL = [Expr1010]), 
[Expr1014] = Scalar Operator((4)&[Expr1012]), 
[Expr1015] = Scalar Operator((16)&[Expr1012])

Expr1014然后Expr1015测试一下标志中的某些位是否打开。 Expr1013如果for的位都为4on和Expr1010is ,则似乎返回一个布尔列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 |
+----------+----------+----------+-------------+----+----+---+---+---+---+

从中我推断出位4的意思是“具有范围的起点”(而不是无界的),而位16的意思是范围的起点是包含端点的。

这6列结果集是从SORT运算符发出的,按排序 Expr1013 DESC, Expr1014 ASC, Expr1010 ASC, Expr1015 DESC。假设True由下式表示1并且False0先前表示的结果集已经在该顺序。

根据我以前的假设,这种最终效果是按照以下顺序显示合并间隔的范围

 ORDER BY 
          HasStartOfRangeAndItIsNullFirst,
          HasUnboundedStartOfRangeFirst,
          StartOfRange,
          StartOfRangeIsInclusiveFirst

合并间隔运算符输出2行

Expr1010    Expr1011    Expr1012
----------- ----------- -----------
NULL        NULL        60
NULL        1048576     10

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

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

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

如果您将查询稍微更改为

select *
from t 
where 
      c2 > 1048576 
   or c2 = 0
;

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

计划显示 Seek Keys

Start: c2 >= 0, End: c2 <= 0, 
Start: c2 > 1048576

SQLKiwi在先前链接的博客文章的评论中给出了为何无法在OP中使用这种简单计划的解释。

索引查找与多个谓词不能混合不同类型的比较谓词(即的IsEq在OP的情况下)。这是该产品只是一个电流限制(和可能解释为何在最后一个查询平等测试的原因c2 = 0实现使用>=<=而不只是直白平等寻求你的查询c2 = 0 OR c2 = 1048576


我无法在Paul的文章中找到任何解释[Expr1012]标记区别的内容。您能推断出60/10表示什么吗?
Mark Storey-

@ MarkStorey-Smith-他说62是为了进行平等比较。我猜60必须要指出的是,除非它是一个明确的标志may(?)或该位指示其他不相关的东西,并且实际上仍然是相等的(如我所做并将其更改为它时),否则> AND < 实际上并没有获得计划中的显示>= AND <=IS NULL260set ansi_nulls offc2 = null60
马丁·史密斯

2
@MartinSmith 60确实用于与NULL比较。范围边界表达式使用NULL表示两端的“无界”。搜索始终是排他性的,即搜索开始:> Expr和结束:<Expr,而不是使用> =和<=包含性。感谢您的博客评论,我将在早上发布答案或更长的评论以作为答复(现在太迟了,现在还不能这样做)。
保罗·怀特说GoFundMonica

@SQLKiwi-谢谢。那讲得通。希望在那之前我会找出一些缺失的地方。
马丁·史密斯

非常感谢您,我仍然在吸收它,但是它似乎很好地解释了问题,剩下的主要问题是您在他的博客上询问@SQLKiwi的问题。我将在几天后进一步思考您的答案,以确保我没有任何后续问题,并且会接受您的答案。再次感谢您,这是巨大的帮助。
Andrew Savinykh

13

常量扫描是SQL Server创建存储桶的一种方式,它将在稍后将其放入执行计划中。我在这里发布了更详尽的解释。要了解持续扫描的目的,您必须进一步研究该计划。在这种情况下,将使用Compute Scalar运算符来填充由恒定扫描创建的空间。

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

真正很酷的部分是该计划是微不足道的。这意味着它经历了最少的优化过程。所有操作都将导致合并间隔。这用于为索引查找创建最小的比较运算符集(有关此内容的详细信息)。

整个想法是消除重叠的值,以便它可以以最小的次数将数据拉出。尽管它仍在使用循环操作,但您会注意到循环仅执行一次,这意味着它实际上是一次扫描。

附录:最后一句不对。有两个搜寻。我看错了计划。其余概念相同,而目标(最少通过)相同。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.