ActiveRecord中的浮点数与十进制数的区别

317
有时,ActiveRecord数据类型会让我感到困惑。额,经常这样。我的永恒问题之一是,在给定的情况下,应该使用 `:decimal` 还是 `:float`?
我经常遇到这个链接:ActiveRecord: :decimal vs :float?,但答案不够清晰,我无法确定:
许多人建议绝对不要使用 float 而始终使用 decimal;也有些人建议仅在科学应用程序中使用 float。
以下是几个示例情况:
- Geolocation/latitude/longitude:-45.756688、120.5777777 等。 - Ratio/percentage:0.9、1.25、1.333、1.4143 等。
过去我使用过 `:decimal`,但与 float 相比,我发现在 Ruby 中处理 BigDecimal 对象非常麻烦。我也知道可以使用 `:integer` 表示货币/分,但对于其他情况,例如数量在其中精度可能随时间变化时,它并不完全适合。
使用每种数据类型的优缺点是什么?有哪些好的经验法则可用于确定使用哪种类型?
3个回答

477

我记得我的计算机科学教授说过,不要在货币方面使用浮点数。

这是因为IEEE规范以二进制格式定义了浮点数。基本上,它存储符号、小数和指数来表示浮点数。就像二进制的科学计数法(类似于+1.43*10^2)。因此,无法精确地存储浮点数中的小数和十进制数。

这就是为什么有一个Decimal格式。如果你这样做:

irb:001:0> "%.47f" % (1.0/10)
=> "0.10000000000000000555111512312578270211815834045" # not "0.1"!

而如果您只是这样做

irb:002:0> (1.0/10).to_s
=> "0.1" # the interprer rounds the number for you

如果你处理的是小数,比如复利或地理位置等问题,我强烈推荐使用十进制格式,因为在十进制格式中,1.0/10恰好等于0.1。

但是需要注意的是,尽管精度较低,浮点数的处理速度更快。以下是一个基准测试:

require "benchmark" 
require "bigdecimal" 

d = BigDecimal.new(3) 
f = Float(3)

time_decimal = Benchmark.measure{ (1..10000000).each { |i| d * d } } 
time_float = Benchmark.measure{ (1..10000000).each { |i| f * f } }

puts time_decimal 
#=> 6.770960 seconds 
puts time_float 
#=> 0.988070 seconds

答案

在不太关注精度的情况下,使用浮点数(float)。例如,一些科学模拟和计算只需要三到四个有效数字。这在权衡准确性和速度时非常有用。因为他们更需要速度而不是精度,所以会使用浮点数。

如果您处理需要精确并且总和正确的数字(如复利和与金钱相关的事物),请使用十进制数(decimal)。记住:如果需要精度,则应始终使用十进制数。


1
如果我理解正确,float 是基于二进制的,而 decimal 是基于十进制的?float 有什么好的用途呢?你的例子是做什么的,能演示一下吗? - Jonathan Allard
1
你是不是指 +1.43*2^10 而不是 +1.43*10^2 - Cameron Martin
69
对于未来的访问者,货币最好的数据类型是整数,而不是十进制数。如果字段的精度为“分”,那么该字段将是以“分”为单位的整数(而不是以“元”为单位的小数)。我曾在银行的IT部门工作过,事务处理就是这样完成的。有些字段具有更高的精度(例如每个“分”的百分之一),但它们仍然是整数。 - adg
1
@adg 是正确的:BigDecimal 也不是货币的好选择。 - Eric Duminil
2
十进制不适合用于货币的建议要么过时,要么是错误的:在像postgres这样的数据库中,它们具有精确的精度,并且与整数完全相同,只是它们可以直接给出正确的小数位。如果您使用整数来表示货币,那么这意味着现在需要应用程序来管理正确缩放金额。在十进制列中建模货币并将其刻度设置为2是完全可以接受的。请参见https://www.postgresql.org/docs/9.1/datatype-numeric.html。甚至在那里他们说:“整数可以被认为具有零的刻度”用于小数。 - AndrewKS
显示剩余10条评论

21
在Rails 3.2.18中,使用SQLServer时,':decimal'变成了':integer',但在SQLite中却可以正常工作。我们通过切换到':float'解决了这个问题。
经验教训是"始终使用同质的开发和部署数据库!"

4
好的,3年后学习Rails之后,我完全同意这个观点。 - Jonathan Allard
3
始终使用同质化的开发和部署数据库! - zx1986
这个线索对我很有帮助。谢谢! - Babajide M. Moibi

18

在Rails 4.1.0中,我遇到了将纬度和经度保存到MySQL数据库的问题。由于浮点数据类型无法保存大分数,因此我将数据类型更改为decimal,这样就可以正常工作了。

  def change
    更改列 :cities, :latitude, :decimal, :precision => 15, :scale => 13
    更改列 :cities, :longitude, :decimal, :precision => 15, :scale => 13
  end

我将我的纬度(:latitude)和经度(:longitude)保存为Postgres中的浮点数,这样做效果很好。 - Scott W
4
@Robikul:是的,那很好,但过于复杂了。对于纬度和经度,使用decimal(13,9)就足够了。@ScottW:我不记得了,但如果Postgres使用IEEE浮点数,它之所以“运行良好”,只是因为您还没有遇到问题......但这对于纬度和经度来说是不充分的格式。最终,您将在最不重要的数字上出现错误。 - Lonny Eachus
@LonnyEachus IEEE浮点数为什么不足以表示纬度/经度? - Alexander Suraphel
6
如果你使用十进制的纬度和经度,那么 IEEE 浮点数会对最低有效位产生误差。举个例子,你的纬度和经度可能有 1 米的精度,但误差可能达到100米甚至更多。如果你在计算中使用它们,这一点尤其重要。 - Lonny Eachus

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