使用Float或Decimal作为会计应用程序的美元金额?

时间:2008-09-15 04:55:26

标签: sql-server vb.net database-design currency accounting

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

你的建议是什么?

Float或Decimal数据类型应该用于美元金额吗?

其中有哪些优点和缺点?

我们每日scrum中提到的一个骗局是,当你计算一个超过两个小数位的结果的数量时,你必须要小心。听起来你必须将金额四舍五入到小数点后两位。

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

专家是Float只占用磁盘上的8个字节,其中Decimal占用9个字节(十进制12,2)

24 个答案:

答案 0 :(得分:106)

  

Float或Decimal数据类型应该用于美元金额吗?

答案很简单。永远不要漂浮。 从不

浮点数根据IEEE 754总是二进制,只有新标准IEEE 754R定义的十进制格式。许多小数二进制部分永远不能等于精确的十进制表示 任何二进制数都可以写为m/2^nmn正整数),任意十进制数为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向上。所以如果结果是 就重要了 0.049999999999 ....或0.0500000000 ...你可能知道它意味着5美分,但是计算机不知道这一点并且轮次0.4999 ...向下(错误)和0.5000 ......向上(右)。
鉴于浮点计算的结果总是包含小的错误项,因此决定是纯粹的运气。如果你想要使用二进制数进行十进制舍入到偶数处理,那就无望了。

不相信?你坚持认为在你的帐户系统中一切都很好吗? 资产和负债相等?好的,然后获取每个条目的每个给定格式化的数字,解析它们并用独立的十进制系统求和它们! 将其与格式化的总和进行比较 糟糕,有些不对劲,不是吗?

  

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

无法解决此错误。因为所有人都自动认为计算机总和正确,所以几乎没有人独立检查。

答案 1 :(得分:41)

答案 2 :(得分:22)

首先你应该阅读这个What Every Computer Scientist Should Know About Floating Point Arithmetic。那么你应该考虑使用某种类型的fixed point / arbitrary-precision number包(例如java BigNum,python十进制模块),否则你将陷入一个受伤的世界。然后弄清楚是否使用本机SQL十进制类型就足够了。

浮点/双打存在(ed)以暴露现在已经过时的快速x87 fp。如果您关心计算的准确性和/或不完全补偿其限制,请不要使用它们。

答案 3 :(得分:10)

作为附加警告,SQL Server和.Net框架使用不同的默认算法进行舍入。请务必查看Math.Round()中的MidPointRounding参数。 .Net框架默认使用Bankers算法,SQL Server使用Symmetric Algorithmic Rounding。查看维基百科文章here

答案 4 :(得分:7)

问问你的会计师!他们会对你使用漂浮物皱眉。就像之前发布的一些,如果您不关心准确性,请仅使用float。虽然在谈到金钱时我会反对它。

在会计软件中不接受浮动。使用带小数点后4位的小数。

答案 5 :(得分:6)

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

例如,你不能存储1/3作为小数,它将是0.3333333333 ...(依此类推)

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

因此1.5被存储为3 x 2到-1(或3/2)

使用这些base-2指数会创建一些奇怪的无理数,例如:

将1.1转换为浮点数然后再将其转换回来,结果如下:1.0999999999989

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

有关my blog上此问题的更多信息,但基本上,对于存储,您最好使用小数。

在Microsoft SQL Server上,您拥有money数据类型 - 这通常最适合财务存储。精确到4位小数。

对于计算,你有更多的问题 - 不准确性是一小部分,但把它放入电源功能,它很快变得重要。

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

答案 6 :(得分:5)

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

答案 7 :(得分:5)

使用SQL server的十进制类型。

请勿使用 money float

钱使用4位小数,比使用小数点快但是有一些明显的和一些不那么明显的舍入问题(see this connect issue

答案 8 :(得分:5)

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

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

财务数据基于几个世纪的纸笔练习以及相关的惯例。它们相当准确,但更重要的是,它们是可重复的。使用各种数字和费率的两名会计师应该提出相同的数字。任何出现差异的空间都是欺诈的余地。

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

答案 9 :(得分:4)

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

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

答案 10 :(得分:4)

对于我帮助开发的银行系统,我负责系统的“应计利息”部分。每天,我的代码都会计算出那天在余额上产生的利息(收入)。

对于那个计算,需要极高的准确性和保真度(我们使用甲骨文的FLOAT),因此我们可以记录累积的“十分之一便士”。

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

这使会计师,审计师和测试员满意。

因此,请咨询您的客户。他们会告诉你他们的银行/会计规则和做法。

答案 11 :(得分:4)

使用Float获取资金的唯一理由是,如果您不关心准确的答案。

答案 12 :(得分:3)

在会计系统中你应该注意的另一件事是没有人应该直接访问这些表。这意味着对会计系统的所有访问都必须通过存储过程进行。这可以防止欺诈,而不仅仅是SQl注入攻击。想要提交欺诈的内部用户不应该有能力直接更改数据库表中的数据。这是对您系统的严格内部控制。你真的想要一些心怀不满的员工去你的数据库的后端并让它开始对他们进行检查吗?或者隐藏他们在没有批准权限的情况下批准未经授权的供应商的费用?整个组织中只有两个人能够直接访问您的财务数据库,dba和备份中的数据。如果您有许多dbas,则只有其中两个应具有此访问权限。

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

答案 13 :(得分:3)

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

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

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

答案 14 :(得分:2)

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

答案 15 :(得分:2)

在100个分数n / 100中,其中n是自然数,使得0 <= n且n <1。 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;
}

答案 16 :(得分:2)

你总是可以为.Net写一些钱类型。

看一下这篇文章:A Money type for the CLR - 笔者在我看来做了出色的工作。

答案 17 :(得分:1)

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

关于小数再占一个字节的Con,我会说不关心它。在100万行中,你只会再使用1 MB,而且这些天存储非常便宜。

答案 18 :(得分:1)

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

答案 19 :(得分:1)

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

答案 20 :(得分:0)

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

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

浮点是计算的理想选择,其中结果的比例是重要的。如果你想要精确到一些小数位,这是一个糟糕的选择。

答案 21 :(得分:0)

始终使用十进制。由于四舍五入问题,Float会给你不准确的值。

答案 22 :(得分:0)

您的会计师希望控制您的回合方式。使用float意味着你将不断地进行舍入,通常使用FORMAT()类型语句,这不是你想要的方式(使用地板/天花板)。

您有货币数据类型(money,smallmoney),应该使用而不是float或real。存储十进制(12,2)将消除您的舍入,但也会在中间步骤中消除它们 - 这实际上不是您在财务应用程序中所需要的。

答案 23 :(得分:0)

这是一篇描述when to use float and decimal的优秀文章。 Float存储近似值,decimal存储精确值。

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

这是一个有趣的例子,表明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