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)
两者产生相同的结果......问题是为什么会发生这种情况?
我们来看看执行计划.
在查询的这个特定运行中,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操作员的详细文章,该文章检查了这种形状的计划.这是保罗怀特关于这个计划形状的另一篇好文章.
最后,带有两行的辅助表与主表连接,对于辅助表中的每一行,聚合索引中的范围搜索从ExpFrom到ExpTo,在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()是非确定性的.总的来说,感觉就像一个bug.
我对SQL Server内部结构一无所知,但我的猜测看起来像这样:有运行时常量函数RAND().NEWID()被错误地归入了这个类别.然后有人注意到人们不希望它以相同的方式RAND()返回相同的ID,因为每次调用返回相同的随机数.他们通过实际重新生成每次NEWID()出现在表达式中的新ID来修补它.但是优化器的整体规则保持不变RAND(),因此更高级别的优化器认为所有调用NEWID()返回相同的值并自由重新排列表达式,NEWID()从而导致意外结果.
关于类似的奇怪行为还有另一个问题NEWID():
答案说有一个Connect错误报告,它被关闭为"无法修复".微软的评论基本上说这种行为是设计的.
优化器不保证标量函数的执行时间或执行次数.这是一个长期建立的宗旨.它是基本的"余地",它允许优化器有足够的自由度来获得查询计划执行方面的重大改进.