謹慎 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

 

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM