前言
我們知道在MySQL中有3種類型可以表示實數,分別是float,double和decimal。關於如何合理得使用這三種類型,網上的答案也層出不窮。但是究竟該選擇哪一種類型,好像並沒有統一的答案,接下來,將通過一個例子來說明什么情況下選擇float,什么情況下選擇double,什么情況下選擇decimal。相信對這個例子的剖析之后,你就會明白什么時候用什么樣的類型
實數類型

舉個例子
假如我們有一張表,用來存儲用戶的積分,表定義如下:
CREATE TABLE `f` (`f1` float(10,2) DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf-8
然后向這個表里插入131072.32的積分值,如下所示
mysql> insert into f value (131072.32);Query OK, 1 row affected (0.00 sec)mysql> select * from f;+-----------+| f1 |+-----------+| 131072.31 |+-----------+1 row in set (0.00 sec)
然后會驚奇的發現,這個用戶的積分少了0.01,雖然這0.01的積分並不足於引起我們的注意,但是問題還是來了
丟失數據是否是正常現象?為什么會少0.01,有沒有可能少0.02,或者少1,少10甚至少100?怎么樣才能讓我們的數據准確?
官方怎么看數據丟失
首先遇到問題,第一想到的就是官方找答案,我們翻閱官方文檔,關於float和double有這樣一段描述
For FLOAT, the SQL standard permits an optional specification of the precision (but not the range of the exponent) in bits following the keywordFLOAT in parentheses. MySQL also supports this optional precision specification, but the precision value is used only to determine storage size. A precision from 0 to 23 results in a 4-byte single-precision FLOAT column. A precision from 24 to 53 results in an 8-byte double-precision DOUBLEcolumn.
這段話大致可以這樣描述:數據的精確度取決於分配給每種數據類型的存儲長度,其中float分配了4字節,而double分配了8字節;並且數據的這種不准確是正常現象。采用float和double本來就是不准的!!
實數保存和分配存儲長度的關系
在MySQL官方里有這樣一句話,數據准確度取決於分配給數據類型存儲的長度。在查閱資料可知,單精度類型float和雙精度類型double在計算機中存儲的時候,由於計算機只能存儲二進制,所以浮點型數據在存儲的時候,必須轉化成二進制。在計算機中,float型數據的存儲格式為
比如8.25用二進制表示可表示為1000.01,轉成指數的形式1.00001*2^3,在計算機中
我們知道對於float類型的數據,只分配了32位的存儲空間,對於double類型值分配了64位,但是並不是所有的實數都能轉成32位或者64位的二進制形式,如果超過了,就會出現截斷,這就是誤差的來源。
比如將上面例子中的131072.32轉成二進制后的數據為:
100000000000000000.0101000111101011100001010001111010111000010100011111…
這是一個無窮數,對於float類型,只能截取前32位進行存儲,對於double只能截取前64位進行存儲。所以
131072.32保存為float類型是存儲形式為:01001000000000000000000000010100;
131072.32保存為double類型的格式為:0100000100000000000000000000001010001111010111000010100011110101
針對float情況,至少我們可以得出結論:
1. 如果一個float型數據轉成二進制后的第32位之后都是0,那么數據是准的
2. 如果一個float型數據轉成二進制后的第32位之后不全為0,則數據就會存在誤差
重新說明float(M, D)兩個參數的意義
這兩個參數表示一共能存M位,其中小數點后占D位。比如float(3,1)表示一共3位,其中小數點后1位數字。這里會有兩個誤區
數據的精度總是能精確到D位,也就是數據的不精確一定出現在小數點后數據存儲的時候只能存儲到D位小數
第一個誤區,如果對於float4字節的存儲空間連整數的存儲不下的時候,連整數都有誤差的,更何況小數,所以存儲空間大小決定存儲精度,和D值無關。來看這樣一個例子
mysql> create table f2 (f1 float(15,2));Query OK, 0 rows affected (0.01 sec)mysql> insert into f2 values (123456789.39);Query OK, 1 row affected (0.00 sec)mysql> select * from f2;+--------------+| f1 |+--------------+| 123456792.00 |+--------------+1 row in set (0.00 sec)
最后你會發現,連整數都不准了,小數被完全抹去了。
第二個誤區,對於存儲而言,是和D無關的一個參數。因為浮點型數據最終都要被轉成二進制進行存儲。並且對於float,這個二進制只能有32位0和1的組合。看下面的例子:
mysql> select * from f;+-----------+| f1 |+-----------+| 131072.31 |+-----------+1 row in set (0.00 sec)mysql> alter table f modify f1 float(10,4);Query OK, 0 rows affected (0.02 sec)Records: 0 Duplicates: 0 Warnings: 0mysql> select * from f;+-------------+| f1 |+-------------+| 131072.3125 |+-------------+1 row in set (0.00 sec)
可以看到,修改一下顯示寬度D,這個時候可以看到MySQL真正存儲的數字是131072.3125
怎么樣才能存儲一個准確的數據
如果采用float或者double類型的話,數據有時候完全准確的,有時候是不准確的,怎么才能存儲一個准確的數字,完全看你需要存什么樣的數據,假如存儲一個8.25這樣的數字,那永遠都是准確的。但是如果存儲0.9這樣的數字,則永遠存不准確。
所以如果一個實數在MySQL中存儲准確的話,會出現以下三種情況
數據真的准確,數據能在有限的存儲空間里完全存儲起來數據存儲被截斷,但是通過四舍五入依然能夠將數據顯示准確數據存儲被截斷,通過四舍五入不能將數字正確顯示
關於decimal類型
通過前面的分析,了解了float和double類型的區別和誤差來源。但是decimal類型是MySQL官方唯一指定能精確存儲的類型,也是DBA強烈推薦和金錢相關的類型都要存儲為decimal類型,如果猜想decimal類型的存儲格式的話,那么一下兩種可以保持數據的准確性
繼續擴大存儲空間,比double更大一個級別,比如128位甚至更多通過字符串化或者其他的方式特殊存儲起來
這兩種方式都能實現decimal精確存儲,但是由於MySQL指定decimal類型最大長度為65.在我們能測試的范圍內,decimal並沒有出現誤差。作為MySQL官方唯一指定精確存儲的decimal類型,后續有精力再研究為什么能做到精確todo
如何選擇float,double,decimal
結論總是放在最后,根據上面的分析:可以得出以下結論
1 如果你要表示的浮點型數據轉成二進制之后能被32位float存儲,或者可以容忍截斷,則使用float,這個范圍大概為要精確保存6位數字左右的浮點型數據
比如10分制的店鋪積分可以用float存儲,小商品零售價格(1000塊之內)
2 如果你要表示的浮點型數據轉成二進制之后能被64位double存儲,或者可以容忍截斷,這個范圍大致要精確到保存13位數字左右的浮點型數據
比如汽車價格,幾千萬的工程造價
3 相比double,已經滿足我們大部分浮點型數據的存儲精度要求,如果還要精益求精,則使用decimal定點型存儲
比如一些科學數據,精度要求很高的金錢
寫在最后
理論上的東西永遠比不上實踐,應用場景大於一切理論。選擇float或者double或者decimal有時候也要看場景,比如我們可以用double存儲一個小商鋪的季度營業額(幾千萬),單獨用double存儲的時候沒有問題,當多個季度,多個年份算總3年內的營業額是,就會出現問題,再也算不出一個准確的答案。所以,如果考慮情況沒那么有把握的情況下,推薦使用decimal,最后,也可以通過其他手段避開這些問題,比如存儲商品價格可以使用 乘於100的形式存儲,展示價格的時候再除於100[完]
轉:http://blog.leanote.com/post/weibo-007/mysql_float_double_decimal
