【问题标题】:Precision nightmare in Java and SQL ServerJava 和 SQL Server 中的精确噩梦
【发布时间】:2009-11-17 16:39:35
【问题描述】:

我一直在与 Java 和 SQL Server 中的精确噩梦作斗争,直到我不知道为止。就个人而言,我理解这个问题及其根本原因,但向全球另一端的客户解释这一点是不可行的(至少对我而言)。

情况是这样的。我在 SQL Server 中有两列 - Qty INT 和 Price FLOAT。这些值是 - 1250 和 10.8601 - 所以为了获得总值,它的数量 * 价格和结果是 13575.124999999998(在 Java 和 SQL Server 中)。这是正确的。问题是这样的——客户不想看到那个,他们只看到这个数字 13575.125 就是这样。在一个地方,他们以 2 个小数精度和另一个以 4 个小数精度查看它。当以 4 位小数显示时,数字是正确的 - 13575.125,但当以 2 位小数显示时,他们认为错误 - 13575.12 - 应该改为 13575.13!

帮助。

【问题讨论】:

    标签: java sql-server rounding precision


    【解决方案1】:

    您的问题是您使用的是浮点数。在 java 方面,您需要使用 BigDecimal,而不是 float 或 double,而在 SQL 方面,您需要使用 Decimal(19,4)(或 Decimal(19,3),如果它有助于跳转到您的精度级别)。不要使用 Money 类型,因为 SQL 中 Money 类型的数学运算会导致截断,而不是舍入。数据存储为浮点类型(您说它是不可更改的)这一事实不会影响这一点,您只需在对其进行数学运算之前一有机会就对其进行转换。

    在您给出的具体示例中,您需要先获取 4 位精度的数字并视情况将其放入 BigDecimal 或 Decimal(19,4) 中,然后将其进一步四舍五入到 2 位精度。然后(如果你四舍五入)你会得到你想要的结果。

    【讨论】:

    • 如果有人偶然发现这篇文章。我们最终将所有 FLOAT 转换为 DECIMAL(18, 2) 或 DECIMAL(18, 6) 以避免小数问题。尽管它仍然是一场噩梦,但它仍然有效。非常感谢所有帮助理解这个问题的人!
    【解决方案2】:

    使用BigDecimal。浮点数不是代表货币的合适类型。它将处理rounding properly. Float 将始终产生舍入错误。

    【讨论】:

      【解决方案3】:

      对于存储货币金额,浮点值不是要走的路。根据您的描述,我可能会处理长度为整数的金额,其值为货币金额乘以 10^5 作为数据库存储格式。

      您需要能够处理不丢失精度的计算,所以这里再次浮点不是要走的路。如果账本中借方和贷方之间的总和相差 1 美分,那么账本在财务人员眼中就失败了,因此请确保您的软件在他们的问题域中运行,而不是在您的问题域中运行。如果您不能将现有类用于货币金额,则需要构建自己的类,该类与 amount * 10^5 一起使用,并根据仅用于输入和输出目的所需的精度进行格式化。

      【讨论】:

      • 正是如此,我们必须处理他们的领域而不是我们的领域。从外观上看,在阅读了所有其他回复之后,似乎我别无选择,只能将数据库范围转换为 MONEY 数据类型。可怕的想法,因为我不知道后果。
      【解决方案4】:

      不要将浮点数据类型用于 价格。您应该使用“金钱”或 “小钱”。

      这是 [MS SQL 的参考资料 数据类型][1]。

      [1]: http://webcoder.info/reference/MSSQLDataTypes.html

      更正:使用 Decimal(19,4)

      谢谢一夏。

      【讨论】:

      • 我希望我能。不幸的是,两个问题(1)只能解决数据库级别的问题(2)数据库无法更改
      • 如果无法更改数据库,则问题不太可能消失,因为任何价格值都可能不准确。您可能会编写一个讨厌的 hack 来使舍入一致,但问题是基础数据总是不准确的。
      • 金钱会导致截断,最好使用 Decimal(19,4) 让 SQL 进行舍入。
      【解决方案5】:

      我想我看到了问题。

      10.8601 无法完美表示,因此虽然四舍五入到 13575.125 工作正常,但很难将其四舍五入到 0.13,因为添加 0.005 并不能完全达到。更糟糕的是,0.005 也没有精确的表示,所以你最终会略低于 0.13。

      然后您的选择是进行两次舍入,一次舍入到三位数,然后一次舍入到 2,或者先进行更好的计算。使用 long 或高精度格式,按 1000 缩放以获得 *.125 到 *125。使用精确整数进行舍入。

      顺便说一句,说“浮点数不准确”的无休止重复变体之一或它总是产生错误并不完全正确。问题是该格式只能表示您可以将两个负幂相加来创建的分数。因此,在 0.01 到 0.99 的序列中,只有 0.25、0.50 和 0.75 具有精确的表示。因此,具有讽刺意味的是,最好使用 FP,通过缩放它以便只使用整数值,然后它与整数数据类型算术一样准确。当然,那你还不如从定点整数开始。

      要小心,例如,将 0.37 缩放到 37 仍然不准确,除非四舍五入。浮点可以用于货币价值,但它的工作量大于它的价值,而且通常没有必要的专业知识。

      【讨论】:

        【解决方案6】:

        FLOAT 数据类型无法准确表示分数,因为它是 base2 而不是 base10。 (查看方便的链接:)http://gregs-blog.com/2007/12/10/dot-net-decimal-type-vs-float-type/)。

        对于金融计算或任何需要准确表示分数的事情,必须使用 DECIMAL 数据类型。

        【讨论】:

          【解决方案7】:

          如果你不能修复底层数据库,你可以像这样修复java:

          import java.text.DecimalFormat;
          
          public class Temp {
          
              public static void main(String[] args) {
                  double d = 13575.124999999;
                  DecimalFormat df2 = new DecimalFormat("#.##");
                  System.out.println( " 2dp: "+ Double.valueOf(df2.format(d)) );
          
                  DecimalFormat df4 = new DecimalFormat("#.####");
                  System.out.println( " 4dp: "+Double.valueOf(df4.format(d)) );
              }
          }
          

          【讨论】:

            【解决方案8】:

            虽然您一开始不应该将价格存储为 float,但您可以考虑将其转换为 decimal(38, 4) 或 money(请注意,money 存在一些问题,因为涉及它的表达式不会动态调整其比例),并在离开 SQL Server 时将其暴露在视图中:

            SELECT Qty * CONVERT(decimal(38, 4), Price)
            

            【讨论】:

              【解决方案9】:

              因此,鉴于您无法更改数据库结构(这可能是最好的选择,因为您使用非固定精度来表示应该是固定/精确的东西,因为许多其他人已经讨论),希望您可以在某处更改代码。在 Java 方面,我认为类似@andy_boot 的回答会起作用。在 SQL 方面,您基本上需要将非精确值转换为您需要的最高精度并从那里继续向下转换,基本上在 SQL 代码中是这样的:

              declare @f  float,
                      @n  numeric(20,4),
                      @m  money;
              
              select  @f = 13575.124999999998,
                      @n = 13575.124999999998,
                      @m = 13575.124999999998
              
              select  @f, @n, @m
              select  cast(@f as numeric(20,4)), cast(cast(@f as numeric(20,4)) as numeric(20,2))
              select  cast(@f as money), cast(cast(@f as money) as numeric(20,2))
              

              【讨论】:

                【解决方案10】:

                你也可以做一个DecimalFormat,然后用它进行四舍五入。

                DecimalFormat df = new DecimalFormat("0.00"); //or "0.0000" for 4 digits.
                df.setRoundingMode(RoundingMode.HALF_UP);
                String displayAmt = df.format((new Float(<your value here>)).doubleValue());
                

                我同意其他人的观点,即您不应该使用 Float 作为数据库字段类型来存储货币。

                【讨论】:

                  【解决方案11】:

                  如果您无法将数据库更改为固定的十进制数据类型,您可以尝试采用 truncate((x+.0055)*10000)/10000 进行舍入。然后 1.124999 将“四舍五入”到 1.13 并给出一致的结果。从数学上讲,这是不可靠的,但我认为它适用于您的情况。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2017-02-06
                    • 2015-10-29
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2013-02-12
                    相关资源
                    最近更新 更多