谨慎 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:})
查看新插入的数据,
可以看到,数字变成了Double 类型。
上面的数据插入是在mongo shell 中 验证的,其实在 nosqlbooster 工具 中,默认也是将数字当成double类型。
3 NumberLong 类型
如果想保留为int类型(64-bit integer),需要显式地通过封装函数NumberLong(),其接受的参数应为string类型。
例如,插入一笔数据
db.testnumber.insertOne( { _id: , calc: NumberLong("") } )
查看插入的数据
mongo shell 客户端查询,显式如下:
我们再来验证下通过mongo shell 工具如何对这一类型进行更新的:
db.collection.updateOne( { _id: },
{ $set: { calc: NumberLong("") } } )
显式指定 封装函数NumberLong()。
查看更新后的数据,
我们再来验证下 long 类型上的 $inc 操作($inc操作符将一个字段的值增加或者减少指定的数值)
db.testnumber.updateOne( { _id: },
... { $inc: { calc: NumberLong() } } )
更新后,查询
上面的例子中,显式地指定了Int64 类型(通过NumberLong()函数),执行前后都是Int64。如果不指定呢?不指定就是默认的Double类型。
继续测试,在原来的基础上再加5.
db.testnumber.updateOne( { _id: },
... { $inc: { calc: } } )
查看显示,
数值的类型由Int64 变成了 Double 类型。
4.32-bit integer (int) 类型
和64-bit integer(long)差不多,不同的是,其转换函数由NumberLong()变成了 NumberInt()
,其接受的参数,也当成string类型来处理。
例如:
db.testnumber.insert({ts:NumberInt("")})
查看插入的数据:
数据类型为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" : , "val" : NumberDecimal( "9.99" ), "description" : "Decimal" })
db.testnumno.insert({ "_id" : , "val" : 9.99, "description" : "Double" })
db.testnumno.insert({ "_id" : , "val" : , "description" : "Double" })
db.testnumno.insert({ "_id" : , "val" : NumberLong(), "description" : "Long" })
db.testnumno.insert({ "_id" : , "val" : NumberDecimal( "10.0" ), "description" : "Decimal" })
Case 1
执行查询
db.testnumno.find({ "val": 9.99 })
返回结果
{ "_id" : , "val" : 9.99, "description" : "Double" }
直接输入数字,默认是Double类型,在算法表示上 double 类型的9.99 和 Decimal 类型的9.99 是不相等的。查询结果只有一条数据。
Case 2
执行查询
db.testnumno.find({ "val": NumberDecimal( "9.99" ) })
返回结果
{ "_id" : , "val" : NumberDecimal("9.99"), "description" : "Decimal" }
返回一条结果的原因和Case 1 相同。
Case 3
执行查询
db.testnumno.find({ val: })
返回结果
{ "_id" : , "val" : , "description" : "Double" }
{ "_id" : , "val" : NumberLong(), "description" : "Long" }
{ "_id" : , "val" : NumberDecimal("10.0"), "description" : "Decimal" }
Case 4
执行查询
db.testnumno.find({ val: NumberDecimal( "" ) })
返回结果
{ "_id" : , "val" : , "description" : "Double" }
{ "_id" : , "val" : NumberLong(), "description" : "Long" }
{ "_id" : , "val" : NumberDecimal("10.0"), "description" : "Decimal" }
Case 5
执行查询
db.testnumno.find({ val: NumberDecimal( "10.0" ) })
返回结果
{ "_id" : , "val" : , "description" : "Double" }
{ "_id" : , "val" : NumberLong(), "description" : "Long" }
{ "_id" : , "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
本文版权归作者所有,未经作者同意不得转载,谢谢配合!!!
谨慎 mongodb 关于数字操作可能导致类型及精度变化的更多相关文章
- java操作Excel处理数字类型的精度损失问题验证
java操作Excel处理数字类型的精度损失问题验证: 场景: CELL_TYPE_NUMERIC-->CELL_TYPE_STRING--->CELL_TYPE_NUMERIC POI版 ...
- 数字操作 转为false的类型 typeof操作符 isNaN函数
console.group('数字操作'); // 浮点数值的内存空间是整数的两倍: // 会alert出来3e-7;从小数点后面6个0开始,就用科学计数法了: //alert(0.0000003); ...
- MongoDB数据库简单操作
之前学过的有mysql数据库,现在我们学习一种非关系型数据库 一.简介 MongoDB是一款强大.灵活.且易于扩展的通用型数据库 MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数 ...
- 【翻译】MongoDB指南/CRUD操作(二)
[原文地址]https://docs.mongodb.com/manual/ MongoDB CRUD操作(二) 主要内容: 更新文档,删除文档,批量写操作,SQL与MongoDB映射图,读隔离(读关 ...
- 【翻译】MongoDB指南/CRUD操作(一)
[原文地址]https://docs.mongodb.com/manual/ MongoDB CRUD操作(一) 主要内容:CRUD操作简介,插入文档,查询文档. CRUD操作包括创建.读取.更新和删 ...
- MongoDB各种查询操作详解
这篇文章主要介绍了MongoDB各种查询操作详解,包括比较查询.关联查询.数组查询等,需要的朋友可以参考下 一.find操作 MongoDB中使用find来进行查询,通过指定find的第一个参数可 ...
- mongodb的常用操作
对于nosql之前工作中有用到bekerlydb,最近开始了解mongodb,先简单写下mongodb的一些常用操作,当是个总结: 1.mongodb使用数据库(database)和集合(collec ...
- 使用Django.core.cache操作Memcached导致性能不稳定的分析过程
使用Django.core.cache操作Memcached导致性能不稳定的分析过程 最近测试一项目,用到了Nginx缓存服务,那可真是快啊!2Gb带宽都轻易耗尽. 不过Api接口无法简单使用Ngin ...
- MongoDB Java API操作很全的整理
MongoDB 是一个基于分布式文件存储的数据库.由 C++ 语言编写,一般生产上建议以共享分片的形式来部署. 但是MongoDB官方也提供了其它语言的客户端操作API.如下图所示: 提供了C.C++ ...
随机推荐
- 从0系统学Android--1.2 手把手带你搭建开发环境
要想进行程序开发,首先我们需要搭建开发环境,下面就开始搭建环境. 1.2.1 所需的工具 首先 Android 开发是基于 Java 的,因此你需要掌握简单的 Java 语法.会基础的 Java 语法 ...
- Tiny Counting
也许更好的阅读体验 样例一 输入 4 1 4 3 2 输出 3 样例二 输入 5 9 1 0 0 5 输出 8 题解 这是本人自己想了2个半小时才想出来的方法,稍稍有点复杂但是很好理解 题目的意思就是 ...
- [小米OJ] 9. 移除 K 位得到最小值
思路: 重复k次: 1.找到并且删除第一个 num[i] > num[i+1] 的第i位数字. 2.若删除过程中,序列变成递增序列,则直接删除最后一位. 注意除去字符串头的0 def solut ...
- python包-logging-hashlib-openpyxl模块-深浅拷贝-04
包 包: # 包是一系列模块文件的结合体,表现形式是文件夹,该文件夹内部通常会包含一个__init__.py文件,本质上还是一个模块 包呢,就是前两篇博客中提到的,模块的四种表现形式中的第三种 # 把 ...
- NetCore跨平台桌面框架Avalonia的OSX程序打包
虽然工作开发语言已经转到了java,但平时仍会用netcore做一些小工具,提升工作效率,但是笔记本换成了Mac,小工具只能做成命令行形式,很是痛苦,迫切需要一个.net跨平台的桌面程序解决方案. 为 ...
- gawk(awk)的用法案例
gawk(awk)的用法案例 本文首先简单介绍一个gawk和awk的区别,然后是一点基本使用流程,最后是自己做的一个分析数据文件的脚本代码,供大家参考.另外想了解基本流程的入门知识的可以下载附件pdf ...
- 转 java - java基础知识点
转 https://www.cnblogs.com/xdp-gacl/p/3641769.html 1.一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 可 ...
- Maven重新下载未下载完成的jar包
使用maven下载jar包,经常会遇到下载失败的情况,如果失败的jar包过多,或是不清楚到底有那些jar包在下载过程中出现了问题.可通过maven命令重新批量下载未成功的jar包. 1,打开cmd , ...
- JS面向对象编程(一):封装
js是一门基于面向对象编程的语言. 如果我们要把(属性)和(方法)封装成一个对象,甚至要从原型对象生成一个实例,我们应该怎么做呢? 一.生成对象的原始模式 假定把猫看 ...
- 原创:用node.js搭建本地服务模拟接口访问实现数据模拟
前端开发中,数据模拟是必要的,这样就能等后台接口写完,我们直接把接口请求的url地址从本地数据模拟url换成后台真实地址就完成项目了.传参之类的都不用动. 之前网上找了很多类似于mock等感觉都不太实 ...