1. 程式人生 > >使用 float 儲存小數?

使用 float 儲存小數?

很多程式設計師就會使用 float 型別來儲存小數。sql 的 float 型別和其他大多數程式語言的 float 型別一樣,
根據IEEE 754 標準使用二進位制格式編碼實數資料。

但是很多程式設計師並不清楚浮點型別的特性:並不是所有十進位制中描述的資訊都能使用二進位制儲存。

oracle 使用 float 型別表示的是一個精確值,而 BINARY_FLOAT 型別是一個非精確值,使用的是 IEEE 754 標準

十進位制小數在二進位制的表達方式是完全不同的
比如 十進位制的59.95 ,它儲存了二進位制表示中最接近 59.95 的值,用十進位制表示是 59.950000762639

下我們來看看奇蹟的時刻

首先,我們新建一張表 rate

表很簡單,表設計如下

CREATE TABLE `rate` (
    `tid`  int(11) NOT NULL AUTO_INCREMENT ,
    `money`  float NOT NULL ,
    PRIMARY KEY (`tid`)
);

插入 59.95的資料

INSERT INTO rate(tid,money) VALUES ('1','59.95');

查詢值

有些資料庫能夠通過某種方式彌補資料的不精確性,輸出我們所期待的值

SELECT money FROM rate where tid = 1;

如在 MySQL 中這條語句的返回值是 59.95

但 FLOAT 型別的列中實際儲存的資料可能並不完全等於它的值。將這個值擴大 10 倍,就能看到其中的區別

SELECT money * 10 FROM rate where tid = 1;

你可能希望上面那個擴大的查詢返回的結果應該是 599.5 。但實際上返回的卻是 599.5000076293945

在這個例子中,取整後的值與原值的誤差為 千萬分之一,對於大多數的運算來說已經足夠精確了。

然而,對於某些運算來說這樣的錯誤還是不可容忍的。

  • 最簡單的例子就是用 FLOAT 進行比較操作
SELECT * FROM rate where money = 59.95;

Result: empty set: no row match

通常的變通方案是將浮點數視作"近似相等"

SELECT * FROM rate where ABS(money - 59.95) < 0.0000001 ;
  • 另一個由於使用非精確的 FLOAT 造成誤差的情況,是使用合計函式計算很多值的時候。比如,如果使用 SUM() 函式計算一列中的所有值。

解決方法

使用 NUMERIC 或 DECIMAL型別

使用 SQL 中的 NUMERIC 或 DECIMAL 型別來代替 FLOAT 及其他類似的資料型別進行固定精度的小數儲存。

這些資料型別精確地根據你定義時制定的精度來儲存資料。NUMERIC 和 DECIMAL 的優勢在於,它們不會像 FLOAT 型別那樣對儲存的有理數進行舍入操作。假設你輸入59.95,就可以確信實際儲存的資料就是59.95。

  • MySQL中,CPU不支援 DECIMAL 的直接計算,MySQL伺服器自身事先了DECIMAL的高精度計算 DECIMAL。相對而言,CPU 直接支援原生浮點計算,所以浮點運算明顯更快。
  • MySQL 5.0 和更高的版本中 DECIMAL 型別允許最多 65 個數字。然而,這些版本實際上不能在計算中使用這麼大的數字,因為 DECIMAL 只是一種儲存格式,在計算中 DECIMAL 會轉換為 DOUBLE 型別。

可以考慮使用BIGINT

將需要儲存的貨幣單位根據小數的位數乘以相應的倍數即可。假設要儲存財務資料精確到萬分之一,則可以把所有金額乘以一百萬,然後將結果儲存在 BIGINT 裡,這樣可以同時避免浮點儲存計算不精確和精確計算代價高的問題。

合理適用 FLOAT

一般小數儲存 FLOAT 精度其實是足夠的,但是如果是金融、財務資料等需要小數精度比較高的資料的話,最好就不要適用 FLOAT 型別。因為需要額外的空間和計算開銷,所以應該儘量在只對小數進行精確計算時才適用DEICAL。但在資料量比較大的時候,可以適用 BIGINT 代替 DEICAL 。

參考資料

《sql反模式》
《高效能MySQL》