1. 程式人生 > >謹慎 mongodb 關於數字操作可能導致型別及精度變化

謹慎 mongodb 關於數字操作可能導致型別及精度變化

1.問題描述

最近有一個需求,更新Mongo資料庫中 原料 集合的某欄位價格,更新後,程式報錯了,說長度過長了,需要Truncation。

主要錯誤資訊如下:

FormatException: An error occurred while deserializing the XXXXXXXPrice property of class XXXXXXXXXXXXXXXXXXXX: Truncation resulted in data loss.

 

除錯發現,價格這個資料來自於SQL Server資料庫,是decimal(18,4),資料落到Mongodb中也是Decimal型別。DBA通過Mongodb客戶端工具更新後,更新的文件中的價格欄位由Decimal型別變成了Double型別。

此時問題就出現了:

(1):Double型別為15位,原來小數點後面是四位小數,現在不一定了。

(2):精確度變化,導致部分資料失真。

問題出現,我們有必要認認真真學習總結下MongoDB中的數字型別以及其餘mongo shell等常見客戶端工具。

在MongoDB中,關於數值的型別有:

Type Alias Notes
Double “double”  
32-bit integer “int”  
64-bit integer “long”  
Decimal128 “decimal” New in version 3.4

2. 數字預設為double 型別

mongo shell 客戶端預設將數字看成浮點數。

例如,

db.testnumber.find({t1:12345})

檢視新插入的資料,

可以看到,數字變成了Double 型別。

上面的資料插入是在mongo shell 中 驗證的,其實在 nosqlbooster 工具 中,預設也是將數字當成double型別。

3 NumberLong 型別

如果想保留為int型別(64-bit integer),需要顯式地通過封裝函式NumberLong(),其接受的引數應為string型別。

例如,插入一筆資料

db.testnumber.insertOne( { _id: 10, calc: NumberLong("2090845886852") } )

 檢視插入的資料

mongo shell 客戶端查詢,顯式如下:

我們再來驗證下通過mongo shell 工具如何對這一型別進行更新的:

db.collection.updateOne( { _id: 10 },
                      { $set:  { calc: NumberLong("25555550") } } )

顯式指定 封裝函式NumberLong()。

檢視更新後的資料,

我們再來驗證下 long  型別上的 $inc 操作($inc操作符將一個欄位的值增加或者減少指定的數值)

 

db.testnumber.updateOne( { _id: 10 },
...                       { $inc: { calc: NumberLong(5) } } )

更新後,查詢

上面的例子中,顯式地指定了Int64 型別(通過NumberLong()函式),執行前後都是Int64。如果不指定呢?不指定就是預設的Double型別。

繼續測試,在原來的基礎上再加5.

db.testnumber.updateOne( { _id: 10 },
...                       { $inc: { calc: 5 } } )

檢視顯示,

數值的型別由Int64 變成了 Double 型別。

4.32-bit integer (int) 型別

和64-bit integer(long)差不多,不同的是,其轉換函式由NumberLong()變成了 NumberInt() ,其接受的引數,也當成string型別來處理。

例如:

db.testnumber.insert({ts:NumberInt("246")})

檢視插入的資料:

 

 

資料型別為Int32.

5.NumberDecimal

Decimal 這個資料型別是在Mongo 3.4 才開始引入的。新增Decimal數值型別主要是為了記錄、處理貨幣資料 ,例如 財經資料、稅率資料等。有時候,一些科學計算也採用Decimal型別。

因為mongo shell預設將數字當成double型別,所以也是需要顯式的轉換函式NumberDecimal(),其接受引數是string值。

例如:

db.testnumber.insert({ts:NumberDecimal("1000.55")})

查詢顯示:

我們前面,強調說,引數接受型別是string,如何是數字(預設是double型別)也可以,但是有精度丟失的風險,會把數字變成15位(小數點不計算在內)。

例如

 db.testnumber.insert({ts:NumberDecimal(1000.88)})

檢視

{ "_id" : ObjectId("5d5a38fa3e8964310aa46f83"), "ts" : NumberDecimal("1000.88000000000") }

再插入一筆

db.testnumber.insert({ts:NumberDecimal(1000000000.88)})

查詢這一筆資料

{ "_id" : ObjectId("5d5a39103e8964310aa46f84"), "ts" : NumberDecimal("1000000000.88000") }

再插入一筆

db.testnumber.insert({ts:NumberDecimal(10000000000000.88)})

查詢變成了

{ "_id" : ObjectId("5d5a3e343e8964310aa46f86"), "ts" : NumberDecimal("10000000000000.9") }

再如

 

需要注意的是:如果將數字型別資料作為引數傳遞給NumberDecimal(),只能出現在mongo shell工具中,在其他工具中可能報錯。

例如在工具 nosqlbooster 中就報錯。

{
    "message" : "NumberDecimal param must be string.",
    "stack" : "script:1:29"
}

 測試案例如下:

6.mongo shell 操作Decima型別

如果在mongo shell 操作Decimal,需特別小心,其資料型別和精度有可能變化。

Case 1 

Decimal 型別 +   Decimal 型別

Case 2

Decimal 型別 + long 型別

Case 3

Decimal 型別+ Int 型別

Case 4

Decimal 型別 + 數值 型別,即加數是預設的Double型別

Case 5

如果將兩個Decimal欄位相減,會是什麼樣子呢?我們先在mongo shell 段進行測試。

測試資料:

{ "_id" : ObjectId("5d5a50ebbd9dcf1c9b374e11"), "ts1" : NumberDecimal("32222.21111"), "ts2" : NumberDecimal("11222.21111"), "tst" : NumberDecimal("2211.11111") }
{ "_id" : ObjectId("5d5a50f5bd9dcf1c9b374e12"), "ts1" : NumberDecimal("22222.21111"), "ts2" : NumberDecimal("22222.21111"), "tst" : NumberDecimal("11111.11111") }

相減操作,將tst欄位設定為ts1 和 ts2的差值。

 db.testnumber.find({}).forEach(function(item){   item.tst = item.ts1  - item.ts2 ;db.testnumber.save(item) })

查詢相減後的結果:

{ "_id" : ObjectId("5d5a50ebbd9dcf1c9b374e11"), "ts1" : NumberDecimal("32222.21111"), "ts2" : NumberDecimal("11222.21111"), "tst" : NaN }
{ "_id" : ObjectId("5d5a50f5bd9dcf1c9b374e12"), "ts1" : NumberDecimal("22222.21111"), "ts2" : NumberDecimal("22222.21111"), "tst" : NaN }

此時出現了NAN型別。

NaN (not a number)屬性代表一個“不是數字”的值。這個特殊的值是因為運算不能執行而導致的,不能執行的原因要麼是因為其中的運算物件之一非數字(例如, "abc" / 4),要麼是因為運算的結果非數字(例如,除數為零)。

雖然 NaN 意味著“不是數字”,但是它的型別是 Number

Case 6

相加(+)操作,在mongo shell 中驗證:

db.testnumber.find({}).forEach(function(item){   item.tst = item.ts1  + item.ts2 ;db.testnumber.save(item) })

此時類似string拼湊。

Case 7 

相減操作如果發生在其他客戶端工具,例如 nosqlbooster 工具,效果怎麼樣呢?

執行相減命令

 db.testnumber.find({}).forEach(function(item){   item.tst = item.ts1  - item.ts2 ;db.testnumber.save(item) })

結果截圖

可知:在客戶端工具 nosqlbooster 中,兩個Decimal型別資料的差值是Double型別。

Case 8 

在工具nosqlbooster 上執行相加的命令

db.testnumber.find({}).forEach(function(item){   item.tst = item.ts1  + item.ts2 ;db.testnumber.save(item) })  

查詢結果

在客戶端工具 nosqlbooster 中,兩個Decimal型別資料的 和 也是Double型別。

Case 7、Case 8表明 在 客戶端工具 nosqlbooster 中 ,加減兩個decimal型別資料,其結果變成了Double型別。這不是我們想要的結果,極端情況,數字精確度還會變化。

Case 9

最後,我們看一個數據失真的Case

準備測試資料

db.testnumber.insert({    ts1 : NumberDecimal("1747.872"),ts2 : NumberDecimal("51.408"),tst : NumberDecimal("123"))})

執行更新(在nosqlbooster 執行的)

    db.testnumber.find({}).forEach(function(item){   item.tst = item.ts1  - item.ts2 ;db.testnumber.save(item) })

更新後的資料

{ "_id" : ObjectId("5d5b922744b6e6393c6c7693"), "ts1" : NumberDecimal("1747.872"), "ts2" : NumberDecimal("51.408"), "tst" : 1696.4640000000002 }

tst 欄位,變成了Double型別,且計算後的結果是不準確的。

7.保持Decimal 欄位型別及精度的嘗試

那麼有沒有其他寫法,可以保證更新前後資料型別不變並且不會失真呢?

7.1先尋找保持資料型別不變的方法

如果是 nosqlbooster 工具,將要更新的欄位保留為NumberDecimal,其操作命令如下:

 db.testnumber.find({}).forEach(function(item){   db.testnumber.update({"_id":item._id},{$set:{"tst":NumberDecimal(String(item.ts1 - item.ts2))}})})

檢視更新的結果

但是這個命令是不可以在 mongo shell 段執行的,測試如下:

在mongo shell執行如下命令:

db.testnumber.find({}).forEach(function(item){   db.testnumber.update({"_id":item._id},{$set:{"tst":NumberDecimal(String(item.ts1 - item.ts2))}})})

更新結果如下:

上面的資料型別雖然是Decimal,但是數字是NAN。所以不能更新執行。

 7.2 資料不失真問題

還是使用上面第6 部分的Case 資料。

測試前的資料

db.testnumber.insert({    ts1 : NumberDecimal("1747.872"),ts2 : NumberDecimal("51.408"),tst : NumberDecimal("123"))})

執行更新(在nosqlbooster 執行的)

 db.testnumber.find({}).forEach(function(item){   db.testnumber.update({"_id":item._id},{$set:{"tst":NumberDecimal(String(item.ts1 - item.ts2))}})})

更新後的資料

{ "_id" : ObjectId("5d5b922744b6e6393c6c7693"), "ts1" : NumberDecimal("1747.872"), "ts2" : NumberDecimal("51.408"), "tst" : NumberDecimal("1696.4640000000002") }

tst 欄位,已經變成了Decimal型別,但計算後的結果是不準確的。

我們在開篇講過,原來的資料都是儲存了Decimal(18,4)的格式,所以,如果在mongo 命令上新增四捨五入的函式 toFixed(n) , n為要保留的小數位數。

 db.testnumber.find({}).forEach(function(item){   db.testnumber.update({"_id":item._id},{$set:{"tst":NumberDecimal(String((item.ts1 - item.ts2).toFixed(4)))}})})

查詢結果

{ "_id" : ObjectId("5d5b922744b6e6393c6c7693"), "ts1" : NumberDecimal("1747.872"), "ts2" : NumberDecimal("51.408"), "tst" : NumberDecimal("1696.4640") }

這個結果才是我們真正想要的結果。

8.不同數字型別下的比較 查詢 

測試案例所需資料

db.testnumno.insert({ "_id" : 1, "val" : NumberDecimal( "9.99" ), "description" : "Decimal" })
db.testnumno.insert({ "_id" : 2, "val" : 9.99, "description" : "Double" })
db.testnumno.insert({ "_id" : 3, "val" : 10, "description" : "Double" })
db.testnumno.insert({ "_id" : 4, "val" : NumberLong(10), "description" : "Long" })
db.testnumno.insert({ "_id" : 5, "val" : NumberDecimal( "10.0" ), "description" : "Decimal" })

Case 1 

執行查詢

db.testnumno.find({ "val": 9.99 })

返回結果

{ "_id" : 2, "val" : 9.99, "description" : "Double" }

直接輸入數字,預設是Double型別,在演算法表示上 double 型別的9.99 和 Decimal 型別的9.99 是不相等的。查詢結果只有一條資料。

Case 2

執行查詢

db.testnumno.find({ "val": NumberDecimal( "9.99" ) })

返回結果

{ "_id" : 1, "val" : NumberDecimal("9.99"), "description" : "Decimal" }

返回一條結果的原因和Case 1 相同。

Case 3 

執行查詢

db.testnumno.find({  val: 10 })

返回結果

{ "_id" : 3, "val" : 10, "description" : "Double" }
{ "_id" : 4, "val" : NumberLong(10), "description" : "Long" }
{ "_id" : 5, "val" : NumberDecimal("10.0"), "description" : "Decimal" }

Case 4

執行查詢

db.testnumno.find({ val: NumberDecimal( "10" ) })

返回結果

{ "_id" : 3, "val" : 10, "description" : "Double" }
{ "_id" : 4, "val" : NumberLong(10), "description" : "Long" }
{ "_id" : 5, "val" : NumberDecimal("10.0"), "description" : "Decimal" }

Case 5

執行查詢

db.testnumno.find({ val: NumberDecimal( "10.0" ) })

返回結果

{ "_id" : 3, "val" : 10, "description" : "Double" }
{ "_id" : 4, "val" : NumberLong(10), "description" : "Long" }
{ "_id" : 5, "val" : NumberDecimal("10.0"), "description" : "Decimal" }

 

Case 3、Case 4 、Case 5 表明,在表達整數時,doubel 、Decimal 、Long 三者在演算法表達上相等。

以上 5 個Case 在Mongo shell、nosqlbooster 演示結果一樣。

 

 

 參考文獻:

https://docs.microsoft.com/en-us/dotnet/api/system.double?redirectedfrom=MSDN&view=netframework-4.8

https://docs.mongodb.com/manual/core/shell-types/

https://docs.mongodb.com/manual/reference/operator/query/type/index.html

https://www.jianshu.com/p/6b51adc05203

https://stackoverflow.com/questions/5314238/how-do-i-set-the-serialization-options-for-the-geo-values-using-the-official-10g

 https://www.213.name/archives/1147

 

 

本文版權歸作者所有,未經作者同意不得轉載,謝謝配合!