哪种数据类型应该用于货币?

225

看起来 Money 类型被描述为不推荐使用,详情请参见 此处

我的应用程序需要存储货币,我应该使用哪种数据类型?Numeric、Money 还是 FLOAT?


13
如果你已经阅读了整个帖子,Numeric 就是正确的选择。 - razpeitia
对于那些需要处理多种货币并关心存储货币代码以及金额的人,你可能想要查看数据库中的货币建模(SO)和ISO 4217(维基百科)。简短的答案是你需要两列。 - Fabien Snauwaert
1
不要使用money - user330315
8个回答

181

您的来源并非官方。它可以追溯到2011年,我甚至不认识作者。如果货币类型被官方“不鼓励使用”,PostgreSQL会在手册中明确说明-但它没有这样做

如需更正式的来源,请阅读pgsql-general中的此线程(仅本周!),其中包括来自核心开发人员D'Arcy J.M. Cain(money类型的原始作者)和Tom Lane的声明:

相关答案(及评论!)关于最近发布的改进:

基本上,money 的用途非常有限。Postgres Wiki 建议在除了那些狭义定义的情况下尽量避免使用它。相对于 numeric,它的优点在于性能
在 Postgres 中,decimal 只是 numeric 的别名,并且被广泛用于货币数据,是一种“任意精度”类型。The manual

类型numeric可以存储具有非常大位数的数字。 特别推荐用于存储需要精确度的货币金额和其他数量。

个人而言,如果不涉及小数分钱(基本上是在货币有意义的情况下),我喜欢将货币存储为表示美分的 整数。这比其他提到的选项更有效率。


5
邮件列表上有几次讨论表明货币类型至少不被推荐使用,例如:这里:http://postgresql.nabble.com/Money-type-todos-td1964190.html#a1964192 此外公平地说:版本8.2的手册确实将其称为已弃用项:http://www.postgresql.org/docs/8.2/static/datatype-money.html - user330315
14
@a_horse_with_no_name:您的链接是指向2007年的一个帖子,当时8.2版本正在使用,money类型实际已经被废弃。后来的版本中已经修复了这些问题并重新添加了该类型。个人而言,我喜欢将货币存储为代表“分”的整数类型。 - Erwin Brandstetter
2
Erwin,从仅数据库的角度来看,您可能是正确的。但是,如果将Postgresql + Java组合在一起,它并不好(根据我的经验)。阅读您的评论后,我大多数货币字段都使用了MONEY,现在我遇到了这个Java异常:“SQLException occurred: org.postgresql.util.PSQLException: Bad value for type double: 2,500.00”。我已经搜索了谷歌,并没有找到好的解决方案,所以我现在正在进行乏味的任务,将它们全部更改为NUMERIC或DECIMAL!!! - M-D
3
如果您以分的整数形式存储价格,那么对于像汽油价格这样带有小数分的价格,该怎么办? - Kevin
3
在许多情况下,该列仅表示实际货币价值,其中最小可能单位是美分。如果需要使用小数美分,则不能使用“整数”,请考虑使用“数字”。或者您可以将5个十进制位数而不是仅2个十进制位数进行移动,以表示千分之一美分 - 或您需要的实际比例。无论哪种方式都可以覆盖您的用例。 - Erwin Brandstetter
显示剩余6条评论

128

你的选择如下:

  1. bigint:以分为单位存储金额。这是EFTPOS交易使用的方式。
  2. decimal(12,2):精确存储金额,仅保留两位小数。这是大多数总账软件使用的方式。
  3. float:糟糕的想法 - 精度不足。这是一些天真的开发人员使用的方式。

选项2是最常用且最易于使用的方法。将精度(在我的示例中为12,表示总共12个数字)设置为最适合您的大小或小巧即可。

请注意,如果您正在将多个计算结果的交易汇总为具有业务含义的单个值(例如涉及汇率),则精度应该更高,以提供精确的宏观价值;考虑使用类似于decimal(18,8)的内容,使总和准确并且每个值都可以按照分的精度四舍五入显示。


19
如果你在处理任何形式的逆税计算或外汇交易时,需要至少4个小数位,否则会丢失数据。因此,使用numeric(15,4)numeric(15,6)是一个好主意。 - Petrus Theron
5
第四种选择是使用字符串,并在宿主语言中使用等效的非损失十进制类型。 - ioquatix
4
一个定标整数怎么样?如果将10000.045存储为10000045,并使用1000倍的定标因子,那么不会有任何问题。 - PirateApp

123

数值型数据,精度为强制的2个单位。不要使用浮点数或类似浮点数的数据类型来表示货币,因为如果这样做,当财务报告的结尾数字有+/-几美元时,人们会感到不满意。

就我所知,货币类型只是因为历史原因而保留的。

以1伊朗里亚尔 = 0.000030美元为例。如果使用少于5个小数位,则在转换后,1 IRR 将被四舍五入为0美元。虽然现在我们在谈论纷杂的货币,但我认为在处理货币时,你永远不能太过小心。


15
避免使用浮点数并不是因为这个原因。即使使用Numeric,如果你将一个值除以不能被十的幂整除的数,无论使用什么精度,都会出现舍入误差。(精度为2是个坏主意...请查看文档。) - Doradus
11
如果您想支持任意货币,请勿将比例设置为2(在postgresql术语中,精度是所有数字的数量,例如,在121.121中,它等于6)。有些货币,如巴林第纳尔,其中1000个子单位等于一个单位,还有一些货币根本没有子单位。 - Nikolay Arhipov
2
numeric(3,2) will be able to store max 9.99 3-2 = 1 - Konrad
35
不要这样做!除非您计划在将任何值加载到其他语言之前乘以100,然后使用整数进行数学运算 - 否则您将得到错误的结果。以分(您正在处理的最小货币单位)为单位存储数据,可以避免麻烦。在许多情况下,这是非常糟糕的答案。 - Avamander
2
补充一下@NikolayArhipov所说的,拿这个作为例子:1伊朗里亚尔等于0.000030美元。如果使用少于5位小数,则在转换后1 IRR将舍入为0美元。我知道我们在分居里亚尔,但我认为在处理金钱时你永远不能过于安全。我会使用6位小数。 - Juliusz Gonera
显示剩余2条评论

65

使用64位整数存储为bigint

如果分(cents)不够精细,则存储在小货币单位中或使用大乘数创建较大的整数。我建议使用微美元,其中美元被分成100万份。

例如:$5,123.56 可以存储为 5123560000 微美元。

  • 易于使用,兼容所有语言。
  • 具有足够的精度来处理一分钱的分数。
  • 适用于非常小的单价定价(如广告展示或API费用)。
  • 比字符串或数值数据类型占用更小的存储空间。
  • 易于在计算过程中维护准确性并在最终输出时进行舍入。

4
这是最正确的答案,任何合理的软件都处理手头的最小货币单位。在整数上进行所有计算意味着您不必处理任何特定于语言的浮点数问题。 - Avamander
2
为什么要使用这个,而不是另一个答案中建议的 numeric(15,6) - Juliusz Gonera
1
@JuliuszGonera 基于答案中列出的原因。整数更小且得到广泛支持,避免了所有的数学截断问题。它基本上是使用数字,但移动小数点,使您拥有一个更兼容的整数。 - Mani Gandham
4
啊,对了,我漏掉了关于存储的部分。谢谢!关于“易于使用且与所有语言兼容”,不幸的是,JavaScript 支持的整数最大值为 9007199254740991,比 bigint 的最大值小 1000 倍以上。虽然有 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt,但它的支持有限(目前)并且存在一些注意事项(例如,在进行货币转换时,无法轻松地将其乘以浮点数)。考虑到您可以使用微元存储在 JS 整数中的最大值为 90 亿美元,这对于大多数情况可能仍然足够好。 - Juliusz Gonera
6
我深入挖掘了一下。如果你想支持加密货币,那么 bigint 可能也不够用。比特币提供8位小数的精度,门罗提供12位,以太坊则提供18位。事实上,以太坊虚拟机内部使用uint256,这相当于4个 bigint(无符号的,所以你可能只需使用 numeric(72,18))。话虽如此,对于某些应用程序来说,存储如此精确的值可能并不必要(例如,如果您对它们没有进行任何数学计算),因此我现在将坚持使用 bigint。最坏的情况是,可以使用数据库迁移来解决 :) - Juliusz Gonera

35

我把所有的货币字段设为:

numeric(15,6)

这似乎有些多余,但如果你甚至有一点可能需要处理多种货币,你将需要这么多精度来进行转换。无论我向用户展示什么,我总是以美元储存。这样我就可以根据当天的汇率轻松地转换到其他任何货币。

如果你从不处理多种货币,最糟糕的是你浪费了一些空间来存储一些零。


8
由于没有截断,这会存在错误结果的风险。例如,如果非零值无意间泄漏到剩余的小数位中,比如一个价格字段包含0.333333美元,那么你可能会出现这样一种情况:系统显示某人购买了三件商品,每件售价为0.33美元,总共为1.00美元,而不是0.99美元。请注意,此时的结果已经是错误的。 - Peteris
1
Perteris,那你有什么其他建议吗?无论你投入多少精度,舍入都可能成为一个问题。即使这种方法并不理想,但我还没有找到更好的方法。 - Michael Collette
3
在适当的位置使用固定小数点和截断。一旦您达到一个可储存的货币价值,例如向客户提供的价格,它应该采用适当的度量单位,这在标准零售环境中通常是整分。如果您有不同的业务需求(例如按单位计算高销量商品的价格),可能会有不同的精度设置,但您必须将 展示与存储一起处理 - 如果您使用 x 位小数(或者反过来,例如整千)来显示货币数字,则必须以相同的精度进行存储,不能少也不能多。 - Peteris
对于许多零售相关的网站,这可能是可行的。我主要处理的项目可能需要一个方​​式,在第一种货币中看到相同的成本,与另一种货币的客户,以及第三种供应商的货币。 - Michael Collette
5
Peteris,这是一个“垃圾输入,垃圾输出”的情况。如果你允许价格字段的值为0.333333而不是0.33,那就是应用程序错误。存储更高精度对于货币转换非常有用,正如之前提到的那样。 - mikl

24

使用BigInt来存储货币,将其作为表示最小货币单位的正整数(例如,100美分表示1.00美元,或者100日元表示¥100(一种零点货币))。这就是Stripe所做的——全球电子商务最重要的金融服务公司之一。

来源:请参见https://stripe.com/docs/currencies的“零点货币”

最终这将成为顶尖答案...


6
这就是Stripe的功能。您对此声明有来源吗? - Ivaylo Toskov
零小数货币 所有API请求都期望以货币的最小单位提供金额。例如,要收取10美元,请提供1000的金额值(即1000美分)。对于零小数货币,仍然提供整数金额,但不要乘以100。例如,要收取500日元,请提供500的金额值。 来源:https://stripe.com/docs/currencies - ChatGPT
4
@NarutoSempai提供的链接涉及发票中的定价,而非Stripe在其API中表示货币价值的方式。 - ChatGPT
1
所有API请求都希望以货币的最小单位提供金额。例如,要收取10美元,请提供一个金额值为1000(即1000美分)。 - undefined
如果人们仍然使用SO,这将是最受欢迎的答案... - undefined

0

这不是直接的答案,而是一个关于为什么 float 不是最好的货币数据类型的例子。

由于浮点数在内部的表示方式,更容易出现舍入误差。

在我们自己的十进制系统中,在除以2或5以外的任何东西时,都会出现舍入误差,这些都是10的因数。在二进制中,只有2而没有5,所以即使是“干净”的小数,如0.2(1/5),也存在风险。

如果您尝试以下操作,就可以看到这一点:

select
    0.1::float + 0.2::float as floats,          --  0.30000000000000004
    0.1::numeric + 0.2::numeric as numerics     --- 0.3
;

这就是让审计员抓狂的事情。


-1

个人建议是根据您的需求使用小数精度。如果您想要存储货币最小单位(例如分)的整数数字,并且在编程语言中处理小数存在问题,那么精度为0的小数可以是一个选择。

要确定所需的精度,需要考虑以下因素:

  • 您支持的货币类型(它们可以有不同数量的小数位)。加密货币最多具有18位小数位(ETH)。由于通胀的原因,小数位数可以随时间而变化。
  • 存储商品小单位的价格(可能是从另一种货币转换而来),或者拥有累加器(将1美分交易的10%费用累积,直到总额达到1美分)可能需要使用比货币定义的更多的小数位数
  • 存储最小单位的整数数字可能会导致将来需要重新缩放值以更改精度的需求。如果使用小数,这要容易得多。

注意,您还需要在使用的编程语言中找到相应的数据类型。

有关更多详细信息和注意事项,请参见文章


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