【问题标题】:Seemingly incorrect Operator Precedence看似不正确的运算符优先级
【发布时间】:2018-08-02 20:43:22
【问题描述】:

我在理解某些运算符优先级时遇到了一些困难,我预计下面的 所有 查询都会失败;这是基于下面的文档,其中将在比较运算符之前评估除法。

t-sql 运算符优先级的 Microsoft 文档:https://docs.microsoft.com/en-us/sql/t-sql/language-elements/operator-precedence-transact-sql?view=sql-server-2017

示例数据和查询

CREATE TABLE #orders(
    price INT
)
GO 

INSERT INTO #orders VALUES 
(1)
,(2)
,(3)
,(4)
,(5)
,(6)
,(7)
,(8)
,(9)
,(10)
,(0)  

GO

/*
the following two queries work
*/
SELECT * 
FROM   #orders 
WHERE  2 / price > 0 
       AND price > 0; 

SELECT * 
FROM   #orders 
WHERE  price > 0 
       AND 2 / price > 0; 

/*
these don't work - getting divide by zero error
*/
SELECT * 
FROM   #orders 
WHERE  2 / price > 0 
       AND price IN ( 1, 2, 3, 4, 
                      5, 6, 7, 8, 
                      9, 10 ); 

SELECT * 
FROM   #orders 
WHERE  price IN ( 1, 2, 3, 4, 
                  5, 6, 7, 8, 
                  9, 10 ) 
       AND 2 / price > 0; 

其中一个工作查询的共享执行计划:https://www.brentozar.com/pastetheplan/?id=HJFH3JWB7

其中一个非工作查询的共享(估计)执行计划:https://www.brentozar.com/pastetheplan/?id=r1Ds2yWSQ

任何帮助理解这种行为将不胜感激!

谢谢,

伊莱

【问题讨论】:

  • 你可以用索引来防止,但很奇怪,对我来说看起来像一个错误。
  • @CetinBasoz 您能否详细说明索引将如何影响查询计划?
  • 在#orders(价格)上创建非聚集索引ix_p;
  • @CetinBasoz 这是一个应该做的事情,虽然它没有解释为什么会这样。
  • 当非聚集索引不比聚集索引更窄时,为什么优化器使用它?

标签: sql-server


【解决方案1】:

好问题。我认为documentation page that you shared 上的语言可能有点不精确,这可能会导致您的困惑。这是一个引用(强调我的):

当复杂表达式有多个运算符时,运算符优先级 确定执行操作的顺序。这 执行顺序会显着影响结果值。

这里的强烈暗示是优先级和执行顺序本质上是相同的,但它们不是。 Eric Lippert explains this 比以往任何时候都好(强调原文)。

Precedence 规则描述了带括号的表达式应该如何 当表达式混合不同种类的 运营商。例如,乘法的优先级高于 另外,所以 2 + 3 x 4 等价于 2 + (3 x 4),而不是 (2 + 3) x 4。

关联性规则描述了括号不足的表达式 当表达式有一堆相同的时候应该用括号括起来 一种运算符。例如,加法是从左到 对,所以 a + b + c 等价于 (a + b) + c,而不是 a + (b + c)。在 普通算术,这两个表达式总是给出相同的 结果;在计算机算术中,它们不一定。 (作为一个 练习你能找到 a、b、c 的值,使得 (a + b) + c 是 不等于 C# 中的 a + (b + c)?)

现在是令人困惑的一个。

求值顺序规则描述了每个操作数在 评估一个表达式。括号只是描述如何 结果被组合在一起; “先做括号”不是规则 C# 的。相反,C# 中的规则是“严格评估每个子表达式 从左到右”。

阅读全文。 Eric 使用 C# 和 C++ 作为他的示例,但他的 cmets 的应用范围更广。正如他所建议的那样,如果您将运算符优先规则视为控制表达式组件的分组方式而不是它们的执行顺序,那么您所看到的行为就更有意义了。这是您可以理解地预期会失败但实际上成功的查询之一的简化​​版本:

declare @t table (x int);
insert @t values (0);

select * from @t where 2 / x > 0 and x > 0;

SQL Server 的运算符优先级规则简单来说就是上面的查询等价于:

-- Same query as above with operator precedence shown explicitly.
select * from @t where ((2 / x) > 0) and (x > 0);

在这种情况下,AND 的有趣之处在于,如果您知道它的一个操作数为假,那么您甚至无需计算另一个操作数就知道整个表达式为假。一些编程语言指定了操作数求值发生的顺序。例如,the && operator in C# 首先计算其左侧操作数,如果为假,则根本不计算其右侧操作数。但是AND operator in T-SQL 没有以任何方式提出任何要求;由优化器选择。

已经有很多文章专门详细探讨了这种行为;例如,here is a pretty detailed one。这有帮助吗?

【讨论】:

    【解决方案2】:

    我认为这与运算符优先级无关,而是与优化器选择评估 where 子句的顺序有关。

    在前两个中,必须过滤行where price > 0首先防止错误。

    由于后两个使用IN,因此必须首先进行除以限制行,然后评估IN 过滤器。这可以通过添加索引来防止......即使ID成为主键,这将创建一个聚集索引,并且您不会收到错误,因为优化器可以在除法之前使用索引并过滤行,或者选择。

    谓词应从price > 0 AND 2 / price > 0 更改为2/price > 0

    【讨论】:

    • 很奇怪,因为先除法不会限制行数。
    • 你能详细说明@CetinBasoz 吗?除法本身不会评估 where 子句的除法部分,where 2 / price > 0
    • 试试说价格 IN (1,3,4)。哪一个限制了行数? IN 还是每行进行除法?如果您要每行进行除法,您有什么限制?顺便说一句,“2/价格”是什么意思?你是说空格会改变结果吗?
    • 我不确定我是否理解您的问题。组合的 where 子句将限制 price in (1,3,4) AND the 2 / price > 0 的行。优化器可以选择评估这些的顺序。因此,它可以先评估 2 / price > 0,然后再评估 (1,3,4) 中的价格,反之亦然。索引使优化器选择一个顺序来防止这个简单示例的错误,但在包含更多连接或 where 子句中的更多条件的更复杂查询中,索引可能不会防止这种情况发生。
    • @scsimon 首先,感谢您的回答和视频,我现在将深入研究这些内容。注意:对于失败的查询,查询计划显示它将IN(1,2,..) 子句转换为单独的[tempdb].[dbo].[#orders].[price]=(1) OR...。这解释了为什么这些失败,尽管任何一个查询都需要过滤掉那些价格 > 0 的记录——这意味着这两个查询都不会运行实际除以零的比较......
    【解决方案3】:

    实际上,我的偏好是更改表达式(您不太可能拥有价格指数 - 或者只是使其具有或不具有指数的稳健性):

    SELECT * 
    FROM   #orders 
    WHERE CASE WHEN price > 0 THEN 2/price ELSE 0 END > 0 AND
          price IN ( 1, 2, 3, 4, 
                      5, 6, 7, 8, 
                      9, 10 );
    

    【讨论】:

    • 我不是在寻找有关如何编写会成功的查询的答案;相反,我试图了解发生了什么。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-07-07
    • 2015-03-20
    • 2021-10-25
    • 2011-06-21
    • 2013-02-24
    相关资源
    最近更新 更多