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

来源:https://www.cnblogs.com/xuliuzai/archive/2019/08/20/11365739.html
-Advertisement-
Play Games

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://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

 

 

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用python鏈接mysql讀入一個表並把它再寫到hbase 里去(九頭蛇萬歲) 先聲明一下需要用的庫: 倆!!: happybase (寫這個的老哥真的happy) pymysql 建議使用anaconda進行相應版本匹配安裝,在裝happybase的時候,conda預設的channel是找不到 ...
  • ranger大數據領域的一個集中式安全管理框架,它可以對諸如hdfs、hive、kafka、storm等組件進行細粒度的許可權控制。本文將介紹部署過程 1. 部署準備 ranger: 進入apach官網下載 http://ranger.apache.org/download.html, 本次使用的是r ...
  • [學習筆記] 3."超"關鍵字(super keyword) Super是一個參考(或說指針)指向他緊鄰的父類(見下麵的例子)。用super可以指向被隱藏的父類的同名成員。 3.1 super指向父類的成員 註意: 下例中:子類和父類都有i,我們一共有兩個i,用super可以指向前一個父類的i。 例 ...
  • --查看表空間的真實使用情況 set linesize 500 pagesize 500 col tablespace_name format a25 col TP_REAL_GB format a15 col TP_REAL_FREE_GB format a20 select all_tp.TP_... ...
  • 1.嚴格檢查輸入變數的類型和格式 2.對用戶名做強校驗 3.對sql中的特殊字元做轉義 4. 同樣轉義 mysqli_real_escape_string()轉義字元串中的特殊字元: ...
  • 1.預編譯機制(一次編譯多次執行,防止sql註入) 2.預編譯機制 ...
  • 背景介紹 從學sklearn時,除了演算法的坎要過,還得學習matplotlib可視化,對我的實踐應用而言,可視化更重要一些,然而matplotlib的易用性和美觀性確實不敢恭維。陸續使用過plotly、seaborn,最終定格在了Bokeh,因為它可以與Flask完美的結合,數據看板的開發難度降低了 ...
  • # 背景介紹 從學sklearn時,除了演算法的坎要過,還得學習matplotlib可視化,對我的實踐應用而言,可視化更重要一些,然而matplotlib的易用性和美觀性確實不敢恭維。陸續使用過plotly、seaborn,最終定格在了Bokeh,因為它可以與Flask完美的結合,數據看板的開發難度降 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...