76

我们正在用 VB.NET 和 SQL Server 重写我们的旧会计系统。我们引入了一个新的 .NET/SQL 程序员团队来进行重写。大多数系统已经使用浮点数完成了美元金额。我编程的遗留系统语言没有浮点数,所以我可能会使用小数。

你的建议是什么?

是否应该将浮点数或小数数据类型用于美元金额?

两者都有哪些优点和缺点?

在我们的每日例会提到的一个缺点是,当您计算返回超过两位小数的结果的金额时,您必须小心。听起来您必须将金额四舍五入到小数点后两位。

另一个缺点是所有显示和打印的金额都必须有一个格式声明,显示两位小数。我注意到有几次没有这样做,而且金额看起来不正确。(即 10.2 或 10.2546)

专业人士是仅浮点方法在磁盘上占用 8 个字节,而十进制将占用 9 个字节(十进制 12,2)。

4

24 回答 24

110

是否应该将浮点数或小数数据类型用于美元金额?

答案很简单。从不漂浮。从来没有

浮点数始终根据IEEE 754二进制,只有新标准IEEE 754R定义的十进制格式。许多小数二进制部分永远不能等于精确的十进制表示。

任何二进制数都可以写成m/2^n( m,n正整数),任何十进制数都可以写成m/(2^n*5^n)。由于二进制缺少素数factor 5,所有二进制数都可以用小数精确表示,反之则不行。

0.3 = 3/(2^1 * 5^1) = 0.3

0.3 = [0.25/0.5] [0.25/0.375] [0.25/3.125] [0.2825/3.125]

          1/4         1/8         1/16          1/32

因此,您最终会得到一个高于或低于给定十进制数的数字。总是。

为什么这很重要?四舍五入。

正常舍入意味着向下 0..4,向上 5..9。所以结果是……还是……你可能知道这意味着 5 美分,但计算机不知道这一点,然后四舍五入……向下(错误)和……向上(正确)。0.0499999999990.05000000000.49990.5000

鉴于浮点计算的结果总是包含小的误差项,这个决定纯属运气。如果你想用二进制数进行十进制四舍五入处理,那就没有希望了。

不服气?您坚持认为在您的帐户系统中一切正常吗?资产和负债相等吗?好的,然后获取每个条目的每个给定的格式化数字,解析它们并用独立的十进制系统对它们求和!

将其与格式化的总和进行比较。哎呀,有点不对劲,不是吗?

对于该计算,需要极高的准确性和保真度(我们使用了 Oracle 的 FLOAT),以便我们可以记录“十亿分之一便士”的计算结果。

它无助于解决此错误。因为所有人都会自动假设计算机的总结是正确的,几乎没有人独立检查。

于 2008-09-15T20:35:25.350 回答
46

这张照片回答:

照片1,CO

这是另一种情况:来自北安普顿的男子收到一封信,说如果他不支付零美元和零美分,他的家就会被没收!

照片2,CO

于 2010-10-23T00:04:15.977 回答
22

首先,您应该阅读每个计算机科学家应该了解的关于浮点运算的知识。那么你真的应该考虑使用某种类型的定点/任意精度数字包(例如,Java BigNum 或 Python 十进制模块)。否则,你将进入一个受伤的世界。然后弄清楚使用原生 SQL 十进制类型是否足够。

浮点数和双精度数的存在 (ed) 以公开现在几乎已经过时的快速x87 浮点协处理器。如果您关心计算的准确性和/或不完全弥补它们的局限性,请不要使用它们。

于 2008-09-15T08:04:29.070 回答
10

作为附加警告,SQL Server 和 .NET 框架使用不同的默认算法进行舍入。确保检查 Math.Round() 中的 MidPointRounding 参数。.NET 框架默认使用银行家的舍入,SQL Server 使用对称算法舍入。在此处查看 Wikipedia 文章。

于 2008-09-15T13:21:45.983 回答
7

问问你的会计师!他们会因为你使用浮动而对你皱眉。就像大卫辛格说的那样,只有在你不关心准确性的情况下才使用浮点数。尽管在金钱方面我总是反对它。

在会计软件中是不可接受的浮点数。使用带四个小数点的小数。

于 2008-09-15T05:22:12.970 回答
6

浮点数有意想不到的无理数。

例如,您不能将 1/3 存储为小数,它将是 0.3333333333...(等等)

浮点数实际上存储为二进制值和 2 指数的幂。

所以 1.5 存储为 3 x 2 到 -1(或 3/2)

使用这些以 2 为底的指数会创建一些奇数的无理数,例如:

将 1.1 转换为浮点数,然后再次将其转换回来,您的结果将类似于:1.0999999999989

这是因为 1.1 的二进制表示实际上是 154811237190861 x 2^-47,超过了 double 可以处理的范围。

有关此问题的更多信息,请参阅我的博客,但基本上,对于存储,您最好使用小数。

在 Microsoft SQL 服务器上,您有money数据类型 - 这通常最适合金融存储。精确到小数点后 4 位。

对于计算,您会遇到更多问题 - 不准确性只是很小的一部分,但将其放入幂函数中,它很快就会变得很重要。

然而,小数对于任何类型的数学都不是很好——例如,没有对小数幂的原生支持。

于 2008-09-15T09:59:38.997 回答
6

这里有一点背景......

没有数字系统可以准确地处理所有实数。所有这些都有其局限性,这包括标准的 IEEE 浮点数和有符号十进制数。IEEE 浮点数每使用一位更准确,但这并不重要。

财务数字基于几个世纪以来的纸笔实践以及相关的惯例。它们相当准确,但更重要的是,它们是可重现的。使用不同数字和费率的两位会计师应该得出相同的数字。任何差异的空间都是欺诈的空间。

因此,对于财务计算,正确的答案是给出与擅长算术的注册会计师相同的答案。这是十进制算术,而不是 IEEE 浮点数。

于 2009-02-09T16:42:15.453 回答
5

使用 SQL Server 的小数类型。

不要使用金钱浮动

money使用四位小数并且比使用小数更快,但是在舍入方面存在一些明显和不那么明显的问题(请参阅此连接问题)。

于 2008-11-03T01:00:38.323 回答
5

我建议使用 64 位整数,以美分存储整个事物。

于 2008-11-03T01:08:31.567 回答
4

浮点数不是精确的表示,可能存在精度问题,例如在添加非常大和非常小的值时。这就是为什么建议将小数类型用于货币,即使精度问题可能非常罕见。

澄清一下,十进制 12,2 类型将准确存储这 14 位数字,而浮点数不会,因为它在内部使用二进制表示。例如,0.01 不能用浮点数精确表示 - 最接近的表示实际上是 0.0099999998

于 2008-09-15T04:58:44.367 回答
4

使用 Float 赚钱的唯一原因是如果您不关心准确的答案。

于 2008-09-15T05:00:04.067 回答
4

对于我帮助开发的银行系统,我负责系统的“应计利息”部分。每天,我的代码都会计算当天余额中累积(赚取)的利息。

对于该计算,需要极高的准确性和保真度(我们使用了 Oracle 的 FLOAT),因此我们可以记录应计的“十亿分之一”。

当涉及“资本化”利息(即,将利息支付回您的账户)时,金额被四舍五入到美分。帐户余额的数据类型为小数点后两位。(事实上​​它更复杂,因为它是一个多币种系统,可以在许多小数位上工作——但我们总是四舍五入到该货币的“便士”)。是的-那里有损失和收益的“分数”,但是当计算机数字被实现(支付或支付的钱)时,它总是真实的货币价值。

这让会计师、审计师和测试人员感到满意。

因此,请与您的客户核实。他们会告诉你他们的银行/会计规则和惯例。

于 2008-09-15T10:42:26.897 回答
3

比使用小数更好的是只使用普通的旧整数(或者可能是某种 bigint)。这样,您始终可以获得尽可能高的精度,但可以指定精度。例如,数字100可能意味着1.00,其格式如下:

int cents = num % 100;
int dollars = (num - cents) / 100;
printf("%d.%02d", dollars, cents);

如果您想获得更高的精度,可以将 100 更改为更大的值,例如:10 ^ n,其中 n 是小数位数。

于 2008-09-16T09:58:31.783 回答
3

在会计系统中您应该注意的另一件事是,任何人都不应直接访问表。这意味着对会计系统的所有访问都必须通过存储过程

这是为了防止欺诈,而不仅仅是SQL 注入攻击。想要进行欺诈的内部用户永远不应该直接更改数据库表中的数据。这是您系统的关键内部控制。

你真的想让一些心怀不满的员工去你的数据库后端,让它开始给他们写支票吗?或者当他们没有批准权限时隐藏他们批准了未经授权的供应商的费用?整个组织中只有两个人应该能够直接访问您的财务数据库中的数据,即您的数据库管理员 (DBA) 和他的备份。如果您有许多 DBA,则只有其中两个应该具有此访问权限。

我提到这一点是因为如果你的程序员在会计系统中使用浮点数,他们可能完全不熟悉内部控制的概念,并且在他们的编程工作中没有考虑到它们。

于 2009-02-09T16:55:22.927 回答
2

您总是可以为 .NET 编写类似 Money 类型的东西。

看看这篇文章:CLR 的 Money 类型。在我看来,作者的工作非常出色。

于 2008-09-21T06:12:24.020 回答
2

我一直在使用 SQL 的货币类型来存储货币值。最近,我不得不使用许多在线支付系统,并注意到其中一些使用整数来存储货币值。在我当前和新的项目中,我已经开始使用整数,我对这个解决方案非常满意。

于 2009-02-09T14:09:47.690 回答
2

在 n/100 的 100 个分数中,其中 n 是一个自然数,使得 0 <= n 且 n < 100,只有四个可以表示为浮点数。看看这个 C 程序的输出:

#include <stdio.h>

int main()
{
    printf("Mapping 100 numbers between 0 and 1 ");
    printf("to their hexadecimal exponential form (HEF).\n");
    printf("Most of them do not equal their HEFs. That means ");
    printf("that their representations as floats ");
    printf("differ from their actual values.\n");
    double f = 0.01;
    int i;
    for (i = 0; i < 100; i++) {
        printf("%1.2f -> %a\n",f*i,f*i);
    }
    printf("Printing 128 'float-compatible' numbers ");
    printf("together with their HEFs for comparison.\n");
    f = 0x1p-7; // ==0.0071825
    for (i = 0; i < 0x80; i++) {
        printf("%1.7f -> %a\n",f*i,f*i);
    }
    return 0;
}
于 2010-10-21T20:19:27.063 回答
1

您是否考虑过使用货币数据类型来存储美元金额?

关于十进制多占用一个字节的缺点,我想说不要关心它。在 100 万行中,您只会多使用 1 MB,而且现在存储非常便宜。

于 2008-09-15T04:59:07.877 回答
1

无论您做什么,都需要注意舍入错误。使用比您显示的精度更高的精度进行计算。

于 2008-09-15T04:59:18.640 回答
1

您可能希望对货币值使用某种形式的定点表示。您还需要调查庄家的四舍五入(也称为“四舍五入”)。它避免了通常的“四舍五入”方法中存在的偏差。

于 2008-09-15T13:19:10.670 回答
0

你的会计师会想要控制你如何四舍五入。使用 float 意味着您将不断舍入,通常使用FORMAT()类型语句,这不是您想要的方式(使用floor/ceiling代替)。

您有货币数据类型 ( money, smallmoney),应该使用它而不是浮点数或实数。存储小数 (12,2) 将消除您的四舍五入,但也会在中间步骤中消除它们 - 这在金融应用程序中根本不是您想要的。

于 2008-09-15T12:48:31.413 回答
0

始终使用十进制。由于舍入问题,浮点数会给你不准确的值。

于 2008-09-15T12:50:46.510 回答
0

浮点数只能表示为底数的负倍数之和的数字 - 对于二进制浮点,当然是二。

二进制浮点中只有四个可以精确表示的小数:0、0.25、0.5 和 0.75。其他一切都是近似值,就像 0.3333... 是十进制算术中 1/3 的近似值一样。

对于结果的规模很重要的计算,浮点是一个不错的选择。如果您试图精确到小数位数,这是一个糟糕的选择。

于 2008-09-15T12:55:35.377 回答
0

这是一篇很好的文章,描述了何时使用 float 和 decimal。Float 存储一个近似值,decimal 存储一个精确值。

总之,像金钱这样的精确值应该使用小数,而像科学测量这样的近似值应该使用浮点数。

这是一个有趣的例子,它表明 float 和 decimal 都能够丢失精度。当添加一个不是整数的数字然后减去相同的数字时,浮点数会导致精度丢失,而小数则不会:

    DECLARE @Float1 float, @Float2 float, @Float3 float, @Float4 float; 
    SET @Float1 = 54; 
    SET @Float2 = 3.1; 
    SET @Float3 = 0 + @Float1 + @Float2; 
    SELECT @Float3 - @Float1 - @Float2 AS "Should be 0";

Should be 0 
---------------------- 
1.13797860024079E-15

当乘以一个非整数并除以相同的数字时,小数会失去精度,而浮点数不会。

DECLARE @Fixed1 decimal(8,4), @Fixed2 decimal(8,4), @Fixed3 decimal(8,4); 
SET @Fixed1 = 54; 
SET @Fixed2 = 0.03; 
SET @Fixed3 = 1 * @Fixed1 / @Fixed2; 
SELECT @Fixed3 / @Fixed1 * @Fixed2 AS "Should be 1";

Should be 1 
--------------------------------------- 
0.99999999999999900
于 2010-10-23T01:58:43.530 回答