【问题标题】: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

但是为什么要这样做呢?

【问题讨论】:

    标签: sql sql-server sql-server-2014


    【解决方案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 在这里没有严格地从左到右可能是有原因的,但据我所知,文档并没有解释它。
    猜你喜欢
    • 1970-01-01
    • 2017-08-18
    • 2018-06-13
    • 1970-01-01
    • 1970-01-01
    • 2022-10-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多