1.問題描述 最近有一個需求,更新Mongo資料庫中 原料 集合的某欄位價格,更新後,程式報錯了,說長度過長了,需要Truncation。 主要錯誤信息如下: 調試發現,價格這個數據來自於SQL Server資料庫,是decimal(18,4),數據落到Mongodb中也是Decimal類型。DBA ...
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://www.213.name/archives/1147
本文版權歸作者所有,未經作者同意不得轉載,謝謝配合!!!