奇怪的SQL Server(TSQL)查询结果与“WHERE”子句中的NEWID()

时间:2016-07-21 08:09:00

标签: sql sql-server sql-server-2012 sql-server-2008-r2 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());

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

Results screen

虽然我理解NEWID()将在每行的基础上重新计算,但是在地狱中没有统计数据可以达到那么多。我在这里写的所有东西都需要产生细致入微的SELECT,删除任何东西都会阻止它发生。顺便提一下,您可以将IN替换为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));

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

DECLARE @someId UNIQUEIDENTIFIER = NEWID();

SELECT *
FROM #wtfTable
WHERE [WtfId] IN (@someId, NEWID());

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

2 个答案:

答案 0 :(得分:9)

让我们看一下执行计划。

plan

seek

在此查询的特定运行中,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;

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

ExpFrom    ExpTo    ExpFlags
@ID1       @ID1     62
@ID2       @ID2     62

内部ExpFlags指定需要哪种范围搜索(<<=>>=)。如果您向IN子句添加更多变量,您将在连接到此帮助程序表的计划中看到它们。

SortMerge Interval运算符确保合并任何可能的重叠范围。查看Merge Interval operator详细信息Fabiano Amorim,其中检查了具有此形状的计划。 Here is another good post关于保罗怀特的这个计划形式。

最后,带有两行的辅助表与主表连接,对于辅助表中的每一行,在ExpFromExpTo的聚簇索引中有一个范围搜索,这是显示在Index Seek运算符中。 Seek运算符显示<>,但这会产生误导。实际比较由Flags值在内部定义。

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

WHERE 
    ([WtfId] >= @ID1 AND [WtfId] < @ID2)
    OR [WtfId] = @ID3

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

所以,有两个寻求:

from @ID1 to @ID1, which returns one row
from @ID2 to @ID2, which returns one row

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

NEWID()如何影响

我们在您的示例中使用NEWID

SELECT *
FROM #wtfTable
WHERE WtfId IN ('00000000-0000-0000-0000-000000000000', NEWID());

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

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

ExpFrom    ExpTo    ExpFlags
0...0      0...0    62
NEWID()    NEWID()  62

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

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

from `0...0`     to `0...0`
from `some_id_1` to `some_id_2`

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

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

例如:

Is it legal for SQL Server to fill PERSISTED columns with data that does not match the definition?

Inconsistent results with NEWID() and PERSISTED computed column

优化工具应该知道NEWID()是非确定性的。总的来说,这感觉就像一个错误。

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

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

NEWID() In Joined Virtual Table Causes Unintended Cross Apply Behavior

答案是有一个Connect bug report,它已关闭,因为&#34;无法修复&#34;。微软的评论基本上说这种行为是设计的。

  

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

答案 1 :(得分:1)

以下查询按预期返回任何内容 内部类型转换会导致意外结果,我想

SELECT *
FROM wtfTable 
WHERE convert(varchar(100),WtfId) = '00000000-0000-0000-0000-000000000000'
or WtfId = NEWID() ;