問題來源
最近在項目中用到了許多浮點數,精度要求較高,小數點後有4位甚至8位的,思考了一下,類似需求在工程計算、數值計算、股票金融、數字貨幣等場景都會出現。
計算機提供了float/double兩種浮點類型的數據來進行科學計算,但計算機中的浮點數據表示是有誤差的,它們並不能準確的表示十進制的小數,在進行高精度計算時會產生誤差,再經過複雜的傳播,誤差就變得很不可控了。
爲了保證結果的準確性,必須使用高精度計算。高精度計算的基本原理是模擬人工計算過程,保留計算過程中的所有數位,從而達到結果的精確性。各類語言及數據庫都提供了對基本浮點類型的支持,擴展庫都會提供相應的高精度數據的支持,在MYSQL中,decimal就是高精度浮點數據類型。後文主要介紹decimal的使用和實現原理。
MYSQL中浮點數據介紹
float/double
MYSQL當中的float/double和我們常見的編程語言當中的float/double是一樣的,分別表示32位單精度和64位雙精度浮點數,在存儲上分別需要4字節和8字節。從浮點的特性考慮,float和double都只能近似表示,無法精確。如下圖所示,a列爲float(10, 4),b列爲double,參考第2行,同一個數131072.32保存在a和b的結果是不同的。在超出了浮點數的表示精度後,會有一定的截斷,從而引起計算結果的誤差。
numeric/decimal
基本用法
decimal(M,D)表示高精度的小數,其中M表示整數加小數的數位,D表示小數部分位數,並且有如下約束:
字段 | 約束 |
---|---|
M | 總精度,整數加小數部分,1 <= M <= 65, 默認M = 10 |
D | 小數部分精度,0 <= D <= 30且D <= M, 默認D = 0 |
SQL標準中,numeric(M,D)表示準確爲M位的小數,而decimal(M,D)表示精度至少爲M,可以比M位多。但在MYSQL中,兩者是一樣的,都只能表示精度爲M位。
存儲實現
MYSQL對decimal的存儲進行了優化。爲了節省空間,MYSQL採用4字節來存儲9位數位。我們知道,9位數字最大爲999999999,但4字節整數最大可以表示21億多,可以達到10位,所以4字節是充足的。整數部分和小數部分是分開存儲的,每9位存儲爲4字節,多餘部分採用額外的字節存儲。對應的額外字節如下:
數位 | 字節 |
---|---|
0 | 0 |
1-2 | 1 |
3-4 | 2 |
5-6 | 3 |
7-9 | 4 |
舉個例子,decimal(18,9)的整數部分和小數部分各有9位,所以兩邊各需要4字節來存儲。decimal(20,6)有14位整數,6位小數,整數部分先用4字節表示9位,餘下5位仍然需要3字節,所以整數部分共7個字節,小數部分則需要3字節。
浮點位或者前綴0不會被保存。那麼MYSQL是怎麼保存負數的呢?負數的存儲是將正數的每個字節取反。參考下面的示例:
我們將1234567890.1234存儲到MYSQL中,設定M=14,D=4.
首先,將整數和小數進行分組:
1 234567890 1234
整數部分低9位可以存儲爲4個字節,即
...... 0D-FB-38-D2 ......
剩下的一位可以存儲成1個字節,
01 0D-FB-38-D2 ......
小數部分,可以用2字節存儲,得如下
01 0D-FB-38-D2 04-D2
對最高位求反,得到
81 0D-FB-38-D2 04-D2
於是,我們得到了這個14位精度數據在MYSQL中的二進制存儲
81 0D FB 38 D2 04 D2
對上述各個字節求反,可以得到-1234567890.1234的存儲表示
7E F2 04 C7 2D FB 2D
由此可見,MYSQL中的decimal是可以實現對小數部分的高精度的,而且在性能上比起一般採用varchar存儲的做法要好,畢竟MYSQL內部採取的是整數分組計算的策略。這也啓發我們,如果要自己實現高精度計算,應該採取類似的思路。
本文至此結束。本系列後續文章會結合源代碼分析MYSQL加減乘除的具體實現細節。