SQL Server 将 A <> B 拆分为 A < B OR A > B,如果 B 是不确定的,则会产生奇怪的结果

Hei*_*nzi 26 sql-server optimization

我们在 SQL Server 中遇到了一个有趣的问题。考虑以下重现示例:

CREATE TABLE #test (s_guid uniqueidentifier PRIMARY KEY);
INSERT INTO #test (s_guid) VALUES ('7E28EFF8-A80A-45E4-BFE0-C13989D69618');

SELECT s_guid FROM #test
WHERE s_guid = '7E28EFF8-A80A-45E4-BFE0-C13989D69618'
  AND s_guid <> NEWID();

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

小提琴

请暂时忘记s_guid <> NEWID()条件似乎完全无用 - 这只是一个最小的重现示例。由于NEWID()匹配某个给定常量的概率极小,因此每次都应评估为 TRUE。

但事实并非如此。运行此查询通常会返回 1 行,但有时(非常频繁,超过 10 次中的 1 次)返回 0 行。我已经在我的系统上使用 SQL Server 2008 复制了它,您可以使用上面链接的小提琴 (SQL Server 2014) 在线复制它。

查看执行计划显示查询分析器显然将条件拆分为s_guid < NEWID() OR s_guid > NEWID()

查询计划截图

...这完全解释了为什么它有时会失败(如果第一个生成的 ID 小于给定的 ID,而第二个生成的 ID 大于给定的 ID)。

是否允许 SQL Server 评估A <> BA < B OR A > B,即使其中一个表达式是不确定的?如果是,它在哪里记录?还是我们发现了一个错误?

有趣的是,AND NOT (s_guid = NEWID())产生相同的执行计划(和相同的随机结果)。

当开发人员想要选择性地排除特定行并使用以下内容时,我们发现了此问题:

s_guid <> ISNULL(@someParameter, NEWID())
Run Code Online (Sandbox Code Playgroud)

作为“捷径”:

(@someParameter IS NULL OR s_guid <> @someParameter)
Run Code Online (Sandbox Code Playgroud)

我正在寻找文档和/或错误确认。代码并不是那么相关,因此不需要解决方法。

Pau*_*ite 22

是否允许 SQL Server 评估A <> BA < B OR A > B,即使其中一个表达式是不确定的?

这是一个有点争议的观点,答案是合格的“是”。

我所知道的最佳讨论是对 Itzik Ben-Gan 的 Connect 错误报告Bug with NEWID 和 Table Expressions 的回答,该报告已关闭,无法修复。Connect 已停用,因此那里的链接指向 Web 存档。可悲的是,由于 Connect 的消亡,许多有用的材料丢失了(或变得更难找到了)。无论如何,来自微软的 Jim Hogg 的最有用的引语是:

这触及了问题的核心 - 是否允许优化更改程序的语义?即:如果一个程序产生某些答案,但运行缓慢,查询优化器使该程序运行得更快,同时改变给定的结果是否合法?

在大喊“不!”之前 (我个人也有这种倾向:-),请考虑:好消息是,在 99% 的情况下,答案是相同的。所以查询优化是一个明显的胜利。坏消息是,如果查询包含副作用代码,那么不同的计划确实会产生不同的结果。而 NEWID() 就是一种暴露差异的副作用(非确定性)“函数”。[实际上,如果您进行实验,您可以设计其他 - 例如,AND 子句的短路评估:使第二个子句抛出算术被零除 - 不同的优化可能会在第一个子句之前执行第二个子句] 这反映了Craig 在该线程其他地方的解释是 SqlServer 不保证何时执行标量运算符。

所以,我们有一个选择:如果我们想在存在非确定性(副作用)代码的情况下保证某种行为——例如,这样 JOIN 的结果遵循嵌套循环执行的语义——那么我们可以使用适当的选项来强制这种行为 - 正如 UC 指出的那样。但是生成的代码将运行缓慢 - 这实际上是阻碍查询优化器的成本。

综上所述,我们正在将查询优化器朝着 NEWID() 的“预期”行为的方向移动 - 为“预期结果”而牺牲性能。

随着时间的推移,这方面行为发生变化的一个例子是NULLIF 与非确定性函数(如 RAND() )一起工作不正确。还有其他类似的情况,例如COALESCE使用子查询会产生意想不到的结果,这些情况也在逐步解决。

吉姆继续说:

关闭循环。. . 我已经与开发团队讨论过这个问题。最终我们决定不改变当前的行为,原因如下:

1) 优化器不保证标量函数的时间或执行次数。这是一个由来已久的信条。这是基本的“余地”,它允许优化器有足够的自由度来获得查询计划执行的显着改进。

2) 这种“每行一次的行为”并不是一个新问题,尽管它没有被广泛讨论。我们开始在 Yukon 版本中调整它的行为。但是在所有情况下都很难准确确定它的确切含义!例如,它是否适用于“在途中”计算到最终结果的中间行?- 在这种情况下,这显然取决于所选择的计划。还是只适用于最终会出现在完成结果中的行?- 这里发生了令人讨厌的递归,我相信你会同意的!

3) 正如我之前提到的,我们默认为“优化性能”——这对 99% 的情况都有好处。可能会改变结果的 1% 的情况很容易被发现 - 副作用“功能”,例如 NEWID - 并且很容易“修复”(因此,交易性能)。再次“优化性能”的默认设置是长期存在的,并被接受。(是的,这不是编译器为传统编程语言选择的立场,但就这样吧)。

所以,我们的建议是:

a) 避免依赖非保证时间和执行次数语义。b) 避免在表表达式中使用 NEWID()。c) 使用 OPTION 强制执行特定行为(交易性能)

希望这个解释有助于澄清我们将这个错误关闭为“不会修复”的原因。


有趣的是,AND NOT (s_guid = NEWID())产生相同的执行计划

这是规范化的结果,在查询编译期间很早就发生了。两个表达式都编译为完全相同的规范化形式,因此生成相同的执行计划。


Dav*_*oft 11

这是记录(有点)here:

查询中指定的函数实际执行的次数可能因优化器构建的执行计划而异。一个例子是由 WHERE 子句中的子查询调用的函数。子查询及其函数的执行次数可能因优化器选择的不同访问路径而异。

用户定义函数

这不是查询计划将多次执行 NEWID() 并更改结果的唯一查询形式。这令人困惑,但实际上对于 NEWID() 可用于密钥生成和随机排序至关重要。

最令人困惑的是,并非所有非确定性函数实际上都像这样。例如,RAND() 和 GETDATE() 每次查询只执行一次。

  • 从来没听说过。可能是例行程序,因为可以针对 BTree 有效地评估 `=`、`&lt;` 和 `&gt;`。 (3认同)

Jos*_*ell 5

对于它的价值,如果你看一下这个旧的 SQL 92 标准文档,关于不等式的要求在“ 8.2 <comparison predicate>”部分描述如下:

1) 设 X 和 Y 为任意两个对应的 <row value constructor element>。设 XV 和 YV 分别为 X 和 Y 表示的值。

[...]

ii) "X <> Y" 为真当且仅当 XV 和 YV 不相等。

[...]

7) 令Rx 和Ry 为<比较谓词> 的两个<行值构造器>,令RXi 和RYi 分别为Rx 和Ry 的第i 个<行值构造器元素>。"Rx <comp op> Ry" 为真、假或未知,如下所示:

[...]

b) "x <> Ry" 为真当且仅当 RXi <> RYi 对于某些 i。

[...]

h) "x <> Ry" 为假当且仅当 "Rx = Ry" 为真。

注意:为了完整性,我包含了 7b 和 7h,因为他们谈论<>比较 - 我不认为在 T-SQL 中实现行值构造函数与多个值的比较,除非我只是严重误解了这句话的意思 - 这很有可能

这是一堆令人困惑的垃圾。但如果你想继续垃圾箱潜水......

认为1.ii 是适用于这种情况的项目,因为我们正在比较“行值构造函数元素”的值。

ii) "X <> Y" 为真当且仅当 XV 和 YV 不相等。

基本上,X <> Y如果X 和 Y 表示的不相等,则它说的是真的。由于X < Y OR X > Y是该谓词的逻辑等效重写,因此优化器使用它非常酷。

该标准没有对与<>比较运算符任一侧的行值构造函数元素的确定性(或其他什么,你知道的)相关的定义施加任何限制。处理一侧的值表达式可能是不确定的这一事实是用户代码的责任。