很多程序員就會使用 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》