奇数SQL Server(TSQL)查询在"WHERE"子句中使用NEWID()

cko*_*ozl 7 sql sql-server sql-server-2008-r2 sql-server-2012 sql-server-2014

这是一个奇怪的问题,但是对于这种行为的解释我有点沮丧:

背景:(不需要知道)

首先,我正在编写一个快速查询并粘贴一个列表,UNIQUERIDENTIFIER并希望它们在一个WHERE X IN (...)子句中是统一的.在过去,我UNIQUERIDENTIFIER在列表顶部使用了一个空(全零),以便我可以粘贴一组UNIQUERIDENTIFIER看起来像这样的统一:,'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'.这一次,为了避免点击零,我插入了一个NEWID()认为碰撞几率几乎不可能的想法,令我惊讶的是,这导致了数千个额外的结果,比如表的50 +%.

开始问题:(你需要知道的部分)

这个查询:

-- SETUP: (i boiled this down to the bare minimum)
-- just creating a table with 500 PK UNIQUERIDENTIFIERs
IF (OBJECT_ID('tempdb..#wtfTable') IS NOT NULL) DROP TABLE #wtfTable; 

CREATE TABLE #wtfTable (WtfId UNIQUEIDENTIFIER PRIMARY KEY);

INSERT INTO #wtfTable 
SELECT TOP(500) NEWID()
FROM master.sys.all_objects o1 (NOLOCK)
CROSS JOIN master.sys.all_objects o2 (NOLOCK);

-- ACTUAL QUERY:
SELECT *
FROM #wtfTable
WHERE [WtfId] IN ('00000000-0000-0000-0000-000000000000', NEWID());
Run Code Online (Sandbox Code Playgroud)

......应该统计产生bupkis.但如果你运行十次左右,你有时会得到大量的选择.例如,在最后一次运行中,我收到了465/500行,这意味着超过93%的行被返回.

结果屏幕

虽然我理解NEWID()将在每行的基础上重新计算,但在地狱中没有一个统计机会它会达到那么多.我在这里写的所有东西都需要产生细微差别SELECT,删除任何东西都会阻止它发生.顺便说一句,你可以IN用a 替换WHERE WtfId = '...' OR WtfId = NEWID()并仍然收到相同的结果.我正在使用SQL SERVER 2014 Standard补丁到目前为止,没有激活奇怪的设置,我知道.

所以那里的任何人都知道这是怎么回事?提前致谢.


编辑:

'00000000-0000-0000-0000-000000000000'是一个红色的鲱鱼,这是一个与整数一起工作的版本:(有趣的是,我需要用整数将表大小提高到1000以产生有问题的查询计划......)

IF (OBJECT_ID('tempdb..#wtfTable') IS NOT NULL) DROP TABLE #wtfTable; 

CREATE TABLE #wtfTable (WtfId INT PRIMARY KEY);

INSERT INTO #wtfTable 
SELECT DISTINCT TOP(1000) CAST(CAST('0x' + LEFT(NEWID(), 8) AS VARBINARY) AS INT)
FROM sys.tables o1 (NOLOCK)
CROSS JOIN sys.tables o2 (NOLOCK);

SELECT *
FROM #wtfTable
WHERE [WtfId] IN (0, CAST(CAST('0x' + LEFT(NEWID(), 8) AS VARBINARY) AS INT));
Run Code Online (Sandbox Code Playgroud)

或者您可以只替换文字UNIQUEIDENTIFIER并执行此操作:

DECLARE @someId UNIQUEIDENTIFIER = NEWID();

SELECT *
FROM #wtfTable
WHERE [WtfId] IN (@someId, NEWID());
Run Code Online (Sandbox Code Playgroud)

两者产生相同的结果......问题是为什么会发生这种情况?

Vla*_*nov 9

我们来看看执行计划.

计划

寻求

在查询的这个特定运行中,Seek返回的51行而不是估计的1行.


下面的实际查询产生形状相同的计划,但它更容易分析它,因为我们有两个变量@ID1@ID2,您可以在计划跟踪.

CREATE TABLE #wtfTable (WtfId UNIQUEIDENTIFIER PRIMARY KEY);

INSERT INTO #wtfTable 
SELECT TOP(500) NEWID()
FROM master.sys.all_objects o1 (NOLOCK)
CROSS JOIN master.sys.all_objects o2 (NOLOCK);

DECLARE @ID1 UNIQUEIDENTIFIER;
DECLARE @ID2 UNIQUEIDENTIFIER;

SELECT TOP(1) @ID1 = WtfId
FROM #wtfTable
ORDER BY WtfId;

SELECT TOP(1) @ID2 = WtfId
FROM #wtfTable
ORDER BY WtfId DESC;

-- ACTUAL QUERY:
SELECT *
FROM #wtfTable
WHERE WtfId IN (@ID1, @ID2);

DROP TABLE #wtfTable;
Run Code Online (Sandbox Code Playgroud)

如果仔细检查此计划中的运算符,您将看到IN查询的一部分转换为包含两行和三列的表.该Concatenation运营商将返回该表.此帮助程序表中的每一行都定义了索引中的搜索范围.

ExpFrom    ExpTo    ExpFlags
@ID1       @ID1     62
@ID2       @ID2     62
Run Code Online (Sandbox Code Playgroud)

内部ExpFlags规定什么样的范围内寻找需要(<,<=,>,>=).如果向IN子句中添加更多变量,则会在连接到此帮助程序表的计划中看到它们.

Sort并且Merge Interval运营商确保合并任何可能的重叠范围.查看Fabiano Amorim关于Merge Interval操作员的详细文章,该文章检查了这种形状的计划.这是保罗怀特关于这个计划形状的另一篇好文章.

最后,带有两行的辅助表与主表连接,对于辅助表中的每一行,聚合索引中的范围搜索从ExpFromExpTo,在Index Seek运算符中显示.该Seek运营商表示<>,但它是一种误导.实际比较由Flags值在内部定义.

如果您有一些不同的范围,例如:

WHERE 
    ([WtfId] >= @ID1 AND [WtfId] < @ID2)
    OR [WtfId] = @ID3
Run Code Online (Sandbox Code Playgroud)

,您仍会看到具有相同搜索谓词的相同形状的计划,但Flags值不同.

所以,有两个寻求:

from @ID1 to @ID1, which returns one row
from @ID2 to @ID2, which returns one row
Run Code Online (Sandbox Code Playgroud)

在带有变量的查询中,内部表达式会导致在需要时从变量中获取值.在查询执行期间,变量的值不会更改,并且所有内容都按预期正常运行.

怎么NEWID()影响它

当我们NEWID在你的例子中使用时:

SELECT *
FROM #wtfTable
WHERE WtfId IN ('00000000-0000-0000-0000-000000000000', NEWID());
Run Code Online (Sandbox Code Playgroud)

计划和所有内部处理与变量相同.

不同之处在于此内部表有效地变为:

ExpFrom    ExpTo    ExpFlags
0...0      0...0    62
NEWID()    NEWID()  62
Run Code Online (Sandbox Code Playgroud)

NEWID()被称为两次.当然,每个调用产生一个不同的值,这偶然会产生一个覆盖表中某些现有值的范围.

聚集索引有两个范围扫描范围

from `0...0`     to `0...0`
from `some_id_1` to `some_id_2`
Run Code Online (Sandbox Code Playgroud)

现在很容易看出这样的查询如何返回一些行,即使NEWID碰撞的可能性非常小.


显然,优化器认为它可以调用NEWID两次而不是记住第一个生成的随机值并在查询中进一步使用它.还有其他情况,优化者调用的NEWID次数多于预期,产生类似的看似不可能的结果.

例如:

SQL Server使用与定义不匹配的数据填充PERSISTED列是否合法?

使用NEWID()和PERSISTED计算列的结果不一致

优化器应该知道这NEWID()是非确定性的.总的来说,感觉就像一个bug.

我对SQL Server内部结构一无所知,但我的猜测看起来像这样:有运行时常量函数RAND().NEWID()被错误地归入了这个类别.然后有人注意到人们不希望它以相同的方式RAND()返回相同的ID,因为每次调用返回相同的随机数.他们通过实际重新生成每次NEWID()出现在表达式中的新ID来修补它.但是优化器的整体规则保持不变RAND(),因此更高级别的优化器认为所有调用NEWID()返回相同的值并自由重新排列表达式,NEWID()从而导致意外结果.


关于类似的奇怪行为还有另一个问题NEWID():

NEWID()在加入的虚拟表中导致意外的交叉应用行为

答案说有一个Connect错误报告,它被关闭为"无法修复".微软的评论基本上说这种行为是设计的.

优化器不保证标量函数的执行时间或执行次数.这是一个长期建立的宗旨.它是基本的"余地",它允许优化器有足够的自由度来获得查询计划执行方面的重大改进.

  • 另一个`NEWID()`bug!关于此计划形式的另一篇文章是http://sqlblog.com/blogs/paul_white/archive/2012/01/18/dynamic-seeks-and-hidden-implicit-conversions.aspx (2认同)