使用 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》