欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

谨慎 mongodb 关于数字操作可能导致类型及精度变化

程序员文章站 2022-05-18 10:17:16
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})

查看新插入的数据,

谨慎 mongodb 关于数字操作可能导致类型及精度变化

可以看到,数字变成了double 类型

上面的数据插入是在mongo shell 中 验证的,其实在 nosqlbooster 工具 中,默认也是将数字当成double类型。

3 numberlong 类型

如果想保留为int类型(64-bit integer),需要显式地通过封装函数numberlong(),其接受的参数应为string类型。

例如,插入一笔数据

db.testnumber.insertone( { _id: 10, calc: numberlong("2090845886852") } )

 查看插入的数据

谨慎 mongodb 关于数字操作可能导致类型及精度变化

mongo shell 客户端查询,显式如下:

谨慎 mongodb 关于数字操作可能导致类型及精度变化

我们再来验证下通过mongo shell 工具如何对这一类型进行更新的:

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

显式指定 封装函数numberlong()

查看更新后的数据,

谨慎 mongodb 关于数字操作可能导致类型及精度变化

我们再来验证下 long  类型上的 $inc 操作($inc操作符将一个字段的值增加或者减少指定的数值)

 

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

更新后,查询

谨慎 mongodb 关于数字操作可能导致类型及精度变化

上面的例子中,显式地指定了int64 类型(通过numberlong()函数),执行前后都是int64。如果不指定呢?不指定就是默认的double类型。

继续测试,在原来的基础上再加5.

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

查看显示,

谨慎 mongodb 关于数字操作可能导致类型及精度变化

数值的类型由int64 变成了 double 类型。

4.32-bit integer (int) 类型

和64-bit integer(long)差不多,不同的是,其转换函数由numberlong()变成了 numberint() ,其接受的参数,也当成string类型来处理。

例如:

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

查看插入的数据:

 

 

谨慎 mongodb 关于数字操作可能导致类型及精度变化

数据类型为int32.

5.numberdecimal

decimal 这个数据类型是在mongo 3.4 才开始引入的。新增decimal数值类型主要是为了记录、处理货币数据 ,例如 财经数据、税率数据等。有时候,一些科学计算也采用decimal类型。

因为mongo shell默认将数字当成double类型,所以也是需要显式的转换函数numberdecimal(),其接受参数是string值。

例如:

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

查询显示:

谨慎 mongodb 关于数字操作可能导致类型及精度变化

我们前面,强调说,参数接受类型是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") }

谨慎 mongodb 关于数字操作可能导致类型及精度变化

再如

 谨慎 mongodb 关于数字操作可能导致类型及精度变化

需要注意的是:如果将数字类型数据作为参数传递给numberdecimal(),只能出现在mongo shell工具中,在其他工具中可能报错。

例如在工具 nosqlbooster 中就报错。

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

 测试案例如下:

谨慎 mongodb 关于数字操作可能导致类型及精度变化

6.mongo shell 操作decima类型

如果在mongo shell 操作decimal,需特别小心,其数据类型和精度有可能变化。

case 1 

decimal 类型 +   decimal 类型

谨慎 mongodb 关于数字操作可能导致类型及精度变化

case 2

decimal 类型 + long 类型

谨慎 mongodb 关于数字操作可能导致类型及精度变化

case 3

decimal 类型+ int 类型

谨慎 mongodb 关于数字操作可能导致类型及精度变化

case 4

decimal 类型 + 数值 类型,即加数是默认的double类型

谨慎 mongodb 关于数字操作可能导致类型及精度变化

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) })

谨慎 mongodb 关于数字操作可能导致类型及精度变化

此时类似string拼凑。

case 7 

相减操作如果发生在其他客户端工具,例如 nosqlbooster 工具,效果怎么样呢?

执行相减命令

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

结果截图

谨慎 mongodb 关于数字操作可能导致类型及精度变化

可知:在客户端工具 nosqlbooster 中,两个decimal类型数据的差值是double类型。

case 8 

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

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

查询结果

谨慎 mongodb 关于数字操作可能导致类型及精度变化

在客户端工具 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))}})})

查看更新的结果

谨慎 mongodb 关于数字操作可能导致类型及精度变化

但是这个命令是不可以在 mongo shell 段执行的,测试如下:

在mongo shell执行如下命令:

db.testnumber.find({}).foreach(function(item){   db.testnumber.update({"_id":item._id},{$set:{"tst":numberdecimal(string(item.ts1 - item.ts2))}})})

更新结果如下:

谨慎 mongodb 关于数字操作可能导致类型及精度变化

上面的数据类型虽然是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

 

 

 

本文版权归作者所有,未经作者同意不得转载,谢谢配合!!!