在十进制列中存储金额 - 需要什么精度和比例?

210

我在数据库中使用小数列来存储货币价值,今天我在考虑使用什么精度和比例。

由于固定宽度的字符列更有效率,我认为小数列也可能是一样的效果。这是吗?

还有,我应该使用什么精度和比例?我正在考虑精度24/8。这是过头了,不够还是可以的呢?


这是我决定要做的:

  • 将转换率(如果适用)作为浮点数存储在交易表本身中
  • 在帐户表中存储货币
  • 交易金额将是DECIMAL(19,4)
  • 所有使用汇率进行的计算都将由我的应用程序处理,以便我控制四舍五入问题

我认为,将转换率存储为浮点数并不是问题,因为它主要是用于参考,并且我将把它转换为小数。

感谢大家宝贵的意见。


3
问问自己:是否真的有必要以十进制形式存储数据?我不能将数据存储为美分/便士->整数吗? - Terence
7
DECIMAL(19, 4)是一个流行的选择,查看这里,还可以参考这里全球货币格式来决定使用多少位小数,希望能帮到你。 - Shaiju T
2
这是一个非常重要和常见的问题,应该重新开放提问。 - stevec
11个回答

228
如果您正在寻找通用方案,我建议使用DECIMAL(19, 4),这是一个常见的选择(通过快速谷歌可以得到相关信息)。我认为这起源于旧的VBA/Access/Jet货币数据类型,在该语言中是第一种固定点小数类型;而Decimal则只在VB6/VBA6/Jet 4.0版本中以“1.0”样式(即未完全实现)出现。
固定点小数值的存储的经验法则是至少存储比所需精度多一个小数位以允许进行四舍五入。将前端的旧的Currency类型映射到后端的DECIMAL(19, 4)类型之一原因是Currency天生具有银行家取整,而DECIMAL(p,s)通过截断进行取整。 DECIMAL的额外小数位允许实现自定义的取整算法,而不是采用供应商的默认方法(对于期望所有以.5结尾的值向远离零方向舍入的设计师来说,银行家取整令人震惊)。
对我来说,DECIMAL(24, 8)听起来有些过头了。大多数货币报价在四到五位小数点之间。我知道有些情况需要8(或更多)位小数的范围,但这是在普通货币金额(例如四位小数)被按比例分配的情况下,暗示应相应降低小数精度(在这种情况下请考虑使用浮点类型)。而现在很少有人有24位小数的钱需要这么高的精度:)
然而,与其采用通用方案,不妨进行一些调研。向您的设计师或领域专家询问可能适用的会计规则:GAAP,EU等。我模糊地记得有一些欧盟州内转账有明确的取整规则,要求保留五位小数,因此使用DECIMAL(p, 6)进行存储。会计师通常喜欢四个小数位。

避免使用SQL Server的MONEY数据类型,因为它在四舍五入时存在严重的精度问题,还有其他可移植性等方面的考虑。请参见Aaron Bertrand的博客


微软和语言设计师选择了银行家舍入,因为硬件设计师选择了它 [需要引用证明?]。例如,它被奉为电气和电子工程师学会(IEEE)标准。而硬件设计师之所以选择它是因为数学家们喜欢它。详情请参阅维基百科;简单概括一下:1906年版本的《概率与误差理论》将其称为“计算机规则”(“计算机”指进行计算的人类)。


3
请参考此答案和此页面,了解为什么语言设计者选择银行家舍入法。 - Nick Chammas
2
onedaywhen:银行家舍入不是由微软引起的。@NickChammas:也不是语言设计者的发明。微软和语言设计者基本上选择它是因为硬件设计者选择了它;例如,它被奉为电气和电子工程师协会(IEEE)标准。而硬件设计者之所以选择它,是因为数学家更喜欢它。参见http://en.wikipedia.org/wiki/Rounding#History;简单概括一下:《概率与误差理论》1906年版称其为“计算机规则”(“计算机”指进行计算的人)。 - phoog
3
@onedaywhen 为什么 DECIMAL(19, 4)DECIMAL(19, 2) 更受欢迎?大多数世界货币只有两位小数。 - cokedude
3
是的,我的观点已经过时了十年!但这是我第三次得票最多的答案,对于我的SO声誉来说贡献颇丰,所以我赚到了 :) - onedaywhen
9
是的,这是一个很好的答案,但现在它已经不再准确了。如果您正在开发加密货币的软件,则比特币的聪是比特币的千万分之一,因此您需要使用 DECIMAL(19,8)。如果您使用以太坊,则需要 DECIMAL(38,18)。 - CodesInTheDark
显示剩余4条评论

124

我们最近实施了一个需要处理多种货币价值并进行转换的系统,并且通过艰辛的过程找出了一些问题。

永远不要在货币计算中使用浮点数

浮点运算引入的不准确性可能直到它们搞砸事情之前都不会被注意到。所有的值都应该存储为整数或定点数类型,如果您选择使用定点数类型,请确保您完全理解该类型在内部做了什么(即,它是否在内部使用整数或浮点类型)。

当您需要进行计算或转换时:

  1. 将值转换为浮点
  2. 计算新值
  3. 四舍五入数字并将其转换回整数

在第3步将浮点数转换回整数时,不要只是进行强制转换-先使用数学函数将其四舍五入。这通常将是round,尽管在特殊情况下它可以是floorceil。了解差异并进行精心选择。

存储数字类型和值一起

如果您只处理一种货币,则这可能不那么重要,但对于我们来说,处理多种货币很重要。我们使用了货币的3个字符代码,例如USD、GBP、JPY、EUR等。

根据情况,还有可能有助于存储:

  • 数字是税前还是税后(税率是多少)
  • 数字是否是转换结果(以及它从何转换而来)

了解您正在处理的数字的精确范围

对于实值,您希望尽可能精确地精确到货币最小单位。这意味着您没有比一美分、一便士、一日元、一分等更小的价值。不要无故存储比这更高的精度值。

在内部,您可以选择处理较小的值,在这种情况下,这是一种不同类型的货币值。确保您的代码知道哪一个是哪一个,并且不会混淆它们。即使在这里,也要避免使用浮点数值。


将所有这些规则相加,我们决定遵循以下规则。在运行代码中,货币使用最小单位的整数进行存储。

class Currency {
   String code;       //  eg "USD"
   int value;         //  eg 2500
   boolean converted;
}

class Price {
   Currency grossValue;
   Currency netValue;
   Tax taxRate;
}
在数据库中,值以以下格式作为字符串存储:
USD:2500

这存储了价值为$25.00的数值。我们之所以能够做到这一点,仅因处理货币的代码不需要在数据库层本身内执行,所以所有值都可以先转换为内存。其他情况也肯定有适用于其他解决方案的情况。


而且,如果我之前没有说清楚,不要使用浮点数!


1
永远不要说永远:有时候金额需要按比例分配,稍后需要重新加起来。例如:将总红利(相对较小)除以发行股份的数量(相对较小),以得到每股净收益。有时候使用浮点数更好 :) - onedaywhen
33
我坚持我的立场。浮点数规范中存在不准确的地方,随着计算次数的增加,这些不准确性会累积。如果你需要存储比分钱更小的值,则需定义所需的精度级别并坚持使用。不要使用浮点数。认真点,这是一个糟糕的想法。 - Marcus Downing
4
这个答案与Douglas Crockford在他的“Crockford on JavaScript系列”中提出的JavaScript最佳实践一致,他建议将所有货币计算都以PENNIES为单位进行,以避免机器在舍入时出错。因此,如果您在JavaScript中处理货币,以这种方式存储值是非常合理的。 - paperreduction
2
@onedaywhen 针对那些每股净额的情况,您可以将按比例分配的金额相加,与原始总额进行比较,并制定处理余数的策略(当使用十进制/整数类型时)。 - Shiv
2
对于Mysql,如果您打算按金额排序,我建议将整数(2500)存储为bigint。在处理大整数时,不要浪费时间使用PHP 32位,升级到64位或Node.JS ;) - Ricky Boyce
显示剩余6条评论

5

在处理MySQL中的货币时,如果您知道您的货币值的精度,请使用DECIMAL(13,2),或者如果您只想要一个快速的足够好的近似值,请使用DOUBLE。

因此,如果您的应用程序需要处理高达一万亿美元(或欧元或英镑)的货币价值,那么这应该可以胜任:

DECIMAL(13, 2)

或者,如果您需要遵守GAAP,那么请使用:

DECIMAL(13, 4)

9
你能否提供GAAP准则中具体的部分链接,而不是一个2500页文档的内容?谢谢。 - marked-down
@ReactingToAngularVues,似乎页面已经发生了改变。很抱歉。 - pollux1er

2

SQL Server的货币数据类型具有小数点后四位。

来自SQL Server 2000 Books Online:

货币数据表示正数或负数金额。在Microsoft® SQL Server™ 2000中,使用money和smallmoney数据类型存储货币数据。货币数据可以存储到小数点后四位的精度。使用money数据类型存储-922,337,203,685,477.5808到+922,337,203,685,477.5807范围内的值(需要8个字节来存储一个值)。使用smallmoney数据类型存储-214,748.3648到214,748.3647范围内的值(需要4个字节来存储一个值)。如果需要更多的小数位数,请改用decimal数据类型。


2
4个小数位可以让你存储世界上最小的货币子单位,如果需要微支付(纳米支付?!)精度,可以进一步降低。我也更喜欢使用DECIMAL而不是特定于DBMS的货币类型,在我的看法中,将这种逻辑保留在应用程序中更加安全。另一种类似的方法是仅使用[长]整数,并在应用程序级别进行¤unit.subunit格式化以便人类可读性(¤ = 货币符号)。

1
如果您正在使用IBM Informix Dynamic Server,则会有一个名为MONEY的类型,它是DECIMAL或NUMERIC类型的次要变体。 它始终是固定点类型(而DECIMAL可以是浮点类型)。 您可以指定1到32的比例和0到32的精度(默认比例为16,精度为2)。 所以,根据您需要存储的内容,您可能会使用DECIMAL(16,2)-仍足够大,以容纳美国联邦赤字,接近每分钱 - 或者您可能会使用更小的范围或更多的小数位。

1
有时候你需要处理小于一分钱的金额,而且有些国际货币使用非常大的面额。例如,你可能会向客户每笔交易收取0.088美分。在我的Oracle数据库中,这些列被定义为NUMBER(20,4)。

1

如果您将在数据库中执行任何算术运算(例如将账单费率相乘等),您可能需要比这里的建议更精确的计算。这是因为,与应用程序代码中使用不到双精度浮点值同样的原因。


这正是我所想的,但涉及货币汇率(即将津巴布韦元转换为美元)。我将在我使用的数据库(psql、sqlite)上进行一些实验,以查看它们如何处理非常小的小数舍入。 - Ivan
此外,在某些数据库管理系统/编程语言中,浮点数是否存在精度问题? - Ivan
3
所有编程语言中,浮点数存在精度问题。 - Marcus Downing
现在最常见的建议是使用任意精度(比如BigDecimal),但很长一段时间内都是双精度(即用double而不是float)。此外,任意精度在某些情况下会带来显著的性能损失。测试绝对是正确的方法。 - Hank Gay

0
在委内瑞拉,我们有一个通货膨胀严重的国家,甚至开始使用不同的货币。在这个国家,我们有美元(USD)、哥伦比亚比索(COP)和委内瑞拉玻利瓦尔(VES)这几种货币。
由于需要进行货币转换,只保留两位小数已经不够用了。
1美元等于40委内瑞拉玻利瓦尔。如果我们将100.15委内瑞拉玻利瓦尔转换成美元,剩下的就是2.50375美元。
很多操作都受到了影响,所以我的建议是:如果你不打算管理多种货币,使用Decimal(x, 2)是可以的。
如果你打算管理多种货币,你应该考虑到需要进行货币转换的操作,这将需要更多的小数位数。这取决于你的转换顺序。
如果你的转换数量级是10,就增加一个额外的小数位数(Decimal(x, 3)),以此类推。
所以在我们的例子中,我们将1美元转换成1000哥伦比亚比索,所以我们增加了4个额外的小数位数(Decimal(x, 6))。

0
这是一个晚回答,但我已经使用过。
DECIMAL(13,2)

我认为这应该允许最多99,999,999,999.99。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接