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

你的建议是什么?

美元金额应该使用浮点型还是小数型数据类型?

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

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

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

优点是浮点数仅占用磁盘上的 8 个字节,而小数则占用 9 个字节(十进制 12,2)

有帮助吗?

解决方案

美元金额应该使用浮点型还是小数型数据类型?

答案很简单。永远不会漂浮。 绝不 !

浮标是根据 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。所以 重要的是结果是否 0.049999999999....或者 0.0500000000...你可能知道这意味着 5 美分,但计算机不知道这一点并进行四舍五入 0.4999...向下(错误)和 0.5000...向上(右)。
鉴于浮点计算的结果总是包含小误差项,这个决定纯粹是运气。如果你想用二进制数进行十进制舍入到偶数处理,那就毫无希望了。

不服气?您坚持认为您的账户系统一切正常?
资产和负债相等吗?好的,然后取出每个条目的每个给定格式化数字,解析它们并用独立的十进制系统将它们求和!将其与格式化的总和进行比较。
糟糕,出了点问题,不是吗?

为了进行计算,需要极高的准确性和忠诚度(我们使用了Oracle的浮标),因此我们可以记录被指控的“一分钱的十亿分之一”。

无助于解决此错误。因为所有人都会自动认为计算机的求和是正确的,所以实际上没有人独立检查。

其他提示

首先你应该阅读这篇文章 每个计算机科学家都应该了解的浮点运算知识. 。那么你真的应该考虑使用某种类型的 定点/任意精度数 包(例如java BigNum,python 小数模块)否则你将陷入痛苦的世界。然后弄清楚使用本机 SQL 十进制类型是否足够。

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

正如附加警告一样,SQL Server 和 .Net 框架使用不同的默认舍入算法。确保检查 Math.Round() 中的 MidPointRounding 参数。.Net框架默认使用Bankers算法,SQL Server使用对称算法舍入。查看维基百科文章 这里

问问你的会计师吧!他们会因为你使用浮动而对你皱眉。就像之前发布的一些文章一样,仅当您不关心准确性时才使用浮动。尽管在金钱方面我总是反对。

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

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

例如,您不能将 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 位。

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

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

使用 SQL 服务器的 小数 类型。

不使用 或者 漂浮.

货币使用 4 位小数,比使用小数更快 遇到一些明显和一些不那么明显的舍入问题(看到这个连接问题)

我建议使用 64 位整数来存储整个内容(以分为单位)。

这里有一些背景......

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

您始终可以为 .Net 编写类似 Money 类型的内容。

看看这篇文章: CLR 的 Money 类型 - 我认为作者做得非常出色。

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

在 100 个分数 n/100 中,其中 n 是自然数,且 0 <= n 且 n < 100,只有 4 个可以表示为浮点数。看一下这个 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;
}

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

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

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

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

您的会计师会想要控制您的舍入方式。使用 float 意味着您将不断舍入,通常使用 FORMAT() 类型语句,这不是您想要的方式(使用下限/上限代替)。

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

始终使用十进制。由于舍入问题,浮动会给您提供不准确的值。

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

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

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

这是一篇优秀的文章,描述了 何时使用浮点数和小数. 。浮点数存储近似值,小数存储精确值。

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

这是一个有趣的示例,表明浮点型和小数型都可能会丢失精度。当添加一个非整数的数字,然后减去该数字时,浮点数会导致精度损失,而小数则不会:

    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
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top