为啥 SQL Server 会改变操作顺序和装箱方式?
Posted
技术标签:
【中文标题】为啥 SQL Server 会改变操作顺序和装箱方式?【英文标题】:Why is SQL Server changing operation order and boxing the way it does?为什么 SQL Server 会改变操作顺序和装箱方式? 【发布时间】:2015-05-11 14:17:32 【问题描述】:四个简单的 SELECT 语句:
SELECT 33883.50 * -1;
SELECT 33883.50 / -1.05;
SELECT 33883.50 * -1 / 1.05;
SELECT (33883.50 * -1) / 1.05;
但结果并不如我所愿:
-33883.50
-32270.000000
-32269.96773000
-32270.000000
第三个结果似乎值得怀疑。我可以看到发生了什么,首先 SQL Server 对此进行评估:
SELECT -1 / 1.05;
得到答案:
-0.952380
然后它接受该答案并使用它来执行此计算:
SELECT 33883.50 * -0.952380;
要得到(错误的)答案:
-32269.96773000
但是为什么要这样做呢?
【问题讨论】:
【参考方案1】:在你的例子中
33883.50 * -1 / 1.05
被评估为
33883.50 * (-1 / 1.05)
而不是
(33883.50 * -1) / 1.05
这会导致精度下降。
我玩了一下。我使用 SQL Sentry Plan Explorer 查看 SQL Server 如何计算表达式的详细信息。例如,
2 * 3 * -4 * 5 * 6
被评估为
((2)*(3)) * ( -((4)*(5))*(6))
我会这样解释。在 T-SQL 中,一元减号为same priority as subtraction,低于乘法。是的,
当一个表达式中的两个运算符具有相同的运算符优先级时 级别,根据他们在 表达。
,但是这里我们有一个表达式,它混合了具有不同优先级的运算符,解析器严格遵循这些优先级。乘法必须先进行,因此它首先计算4 * 5 * 6
,然后将一元减法应用于结果。
通常 (say in C++) 一元减号具有更高的优先级(如按位 NOT),并且此类表达式按预期进行解析和评估。他们应该在 T-SQL 中将一元减号/加号设为与按位 NOT 相同的最高优先级,但他们没有,这就是结果。所以,这不是一个错误,而是一个糟糕的设计决策。它甚至被记录在案,尽管非常模糊。
当您提到 Oracle 时 - 相同的示例在 Oracle 中的工作方式与在 SQL Server 中的不同:
Oracle 的rules for operator precedence 可能与 SQL Server 不同。所要做的就是使一元减去最高优先级。 在评估decimal
类型的表达式时,Oracle 可能有不同的 rules for determining result precision and scale。
Oracle 对中间结果的舍入可能有不同的规则。 SQL Server "将数字转换为精度和小数位数较低的小数或数值时使用舍入"。
Oracle 可能对这些表达式使用完全不同的类型,而不是decimal
。在SQL Server"中,带有小数点的常量会自动转换为数值数据值,使用所需的最小精度和小数位数。例如,常量 12.345 将转换为精度为 5、小数位数为 3 的数值。”
即使decimal
的定义在Oracle 中也可能不同。即使在SQL Server“数字和十进制数据类型的默认最大精度为 38。在 SQL Server 的早期版本中,默认最大值为 28。”
【讨论】:
感谢您的全面回答。我认为这涵盖了异常的原因,这是我问题的重点。所以我接受这是最好的答案:D 我的荣幸。我再次确认 Plan Explorer 是一个很棒的工具。此外,我了解了所有这些细节,decimal
类型在 T-SQL 中是如何工作的。底线:T-SQL 不是 C++。在此之后,我认为我应该检查我的 T-SQL 代码以进行会计计算。【参考方案2】:
你知道BODMAS规则吗?答案是正确的,不是因为 Sql Server,而是基础数学。
首先是Division
,然后是Subtraction
,所以除法总是在减法之前发生
如果你想得到正确的答案,请使用正确的括号
SELECT (33883.50 * -1) / 1.05;
【讨论】:
这不是减法,它是负一元运算符 (-)(如果愿意,可以从零减去)但是根据 msdn 负运算符是在执行顺序除法之后出现的,你是对的。跨度> 同意,除法之后是减法。但正如 Blim 所说,我的 SQL 语句中没有减法。 BODMAS 很好,但它不能处理在同一“级别”定义优先级。在我的例子中,有乘法和除法。根据 BODMAS 的说法,乘法(“Of”)可能出现在除法之前,但事实并非如此,它们实际上具有相同的优先级。 好的,所以我又看了一遍 MSDN 文章。否定运算符确实出现在运算符顺序除法之后,因此您的答案确实有点道理。但是,这不是一个基本的数学问题,无论优先级如何,答案都应该是相同的。正在发生的事情是 SQL Server 正在对部分结果进行四舍五入,然后得到错误的答案作为最终结果。从理论上讲,应该不需要括号,这可以通过对 Oracle 11g 数据库运行相同的查询来证明——它们都返回相同的答案。【参考方案3】:T-SQL 有一个运算符优先级规则。你可以在链接https://msdn.microsoft.com/en-us/library/ms190276.aspx阅读它。
这似乎是关于一元运算符的优先规则。我尝试了以下查询
SELECT 33883.50 * cast(-1 as int) / 1.05;
SELECT 33883.50 * (-1 * 1) / 1.05;
它会返回正确的答案。最好的办法是在要首先出现的表达式上使用括号,并彻底测试。
【讨论】:
文档似乎是错误的,或者至少忽略了这样一个事实,即根据所涉及的操作可能会发生不同的舍入和精度。例如,它表示对于同一级别的所有运算符(例如乘法和除法),运算符将从左到右处理。那个does not explain this behavior very well。 有趣(但并不奇怪),将 SQL Fiddle 更改为针对 Oracle 运行(这需要添加“FROM DUAL”关键字)无论括号如何都会给出相同的结果。 似乎是一元运算符优先级。 Oracle 在其文档docs.oracle.com/cd/B28359_01/server.111/b28286/… 中有它。这并不能完全解释为什么它会这样 @AaronBertrand, this link 解释了 SQL Server 如何在计算中确定decimal
类型的精度和比例。这里我们有一个精度损失的经典例子。将 1 除以接近 1 (1/1.05)
的数字时,如果您打算将其乘以一个大数字,则需要多于(默认)6 位小数才能获得足够准确的结果。当将大数 33883.50 除以 1.05 时,计算结果的精度和比例的默认规则似乎工作得很好。所以,这里的操作顺序很重要。
@Vladimir 我明白为什么会有精度损失。我只是觉得这里的行为与文档描述的操作顺序不符。 a*b/c
的行为应该与(a*b)/c
相同,但在这种情况下,它的行为类似于a*(b/c)
。 SQL Server 在这里没有严格地从左到右可能是有原因的,但据我所知,文档并没有解释它。以上是关于为啥 SQL Server 会改变操作顺序和装箱方式?的主要内容,如果未能解决你的问题,请参考以下文章