一、前言
最近刚好有开发在使用MongoDB数字类型时,遇到的一些的问题,比如失真,便咨询笔者,所以文章对数字类型进行简单讲解,避免开发在使用MongoDB数字类型时,发生失真和失精等问题,避免生产上发生严重事故,尽管可以通过备份和oplog对数据进行精细化恢复,但仍然会比较花时间,影响产品的可用性。
二、类型介绍
我们先来看看MongoDB支持的数字类型,MongoDB3.2版本及以下,支持三种数值类型,即Double、32-bit integer、64-bit integer,简单点说就是double、int和long类型,MongoDB3.4版本以后支持decimal类型,主要是为了支持货币数据,这四种类型分别对应数字1、16、18和19。由于笔者使用的MongoDB版本为3.2,所以不支持decimal类型,笔者实验主要使用mongo shell客户端并偶尔使用GUI工具进行操作,根据自己情况进行使用即可。
1、数字默认为double 类型
mongos> db.user_info.insert({username:'grepwang',age:18})
WriteResult({ "nInserted" : 1 })
mongos> db.user_info.find()
{ "_id" : ObjectId("5e03141b42e5e9996679441e"), "username" : "grepwang", "age" : 18 }
我们可以查看age的类型为double
mongos> db.user_info.find({age:{$type:1}})
{ "_id" : ObjectId("5e03141b42e5e9996679441e"), "username" : "grepwang", "age" : 18 }
或者
mongos> db.user_info.find({age:{$type:"double"}})
{ "_id" : ObjectId("5e03141b42e5e9996679441e"), "username" : "grepwang", "age" : 18 }
如果使用GUI工具查看,就很直观,如下所示
2、NumberLong 类型
将数字保存为long类型,需要显式地通过封装函数NumberLong(),其接受的参数应为string类型,如下
mongos> db.user_info.insert({username:'mark',age:NumberLong(28)})
WriteResult({ "nInserted" : 1 })
mongos> db.user_info.find({username:'mark'})
{ "_id" : ObjectId("5e0316d842e5e9996679441f"), "username" : "mark", "age" : NumberLong(28) }
验证一下是不是long类型
mongos> db.user_info.find({age:{$type:"long"}})
{ "_id" : ObjectId("5e0316d842e5e9996679441f"), "username" : "mark", "age" : NumberLong(28) }
或者
mongos> db.user_info.find({age:{$type:18}})
{ "_id" : ObjectId("5e0316d842e5e9996679441f"), "username" : "mark", "age" : NumberLong(28) }
如果需要对这一类型更新的话,也需要显示指定NumberLong()函数,这样子age的类型仍然为long类型,假如mark过完生日就29岁了,如下
mongos> db.user_info.update({ "_id" : ObjectId("5e0316d842e5e9996679441f")},{$set:{age:NumberLong("29")})
mongos> db.user_info.find({username:'mark'})
{ "_id" : ObjectId("5e0316d842e5e9996679441f"), "username" : "mark", "age" : NumberLong(29) }
我们再来验证下 对long 类型的age字段进行$inc 操作,如下
mongos> db.user_info.update({"_id" : ObjectId("5e0316d842e5e9996679441f")},{$inc:{ age: 1}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }
我们使用GUI查看一下用户mark的数据,如下所示
发现了吗?age的类型从long变成了double
3、int类型
和long类型类似,不同的是转换函数是NumberInt(),传递的也是一个字符串参数,如下
mongos> db.user_info.insert({username:'lusi',age:NumberInt("20")})
WriteResult({ "nInserted" : 1 })
mongos> db.user_info.find({age:{$type:16}})
{ "_id" : ObjectId("5e03225742e5e99966794420"), "username" : "lusi", "age" : 20 }
数字类型为int32
4、decimal类型
为了便于测试,笔者临时搭建了一个MongoDB,版本为MongoDB3.6.13,使用Mongo shell插入decimal类型数据,需要使用NumberDecimal()这个转换函数,参数也是字符串,如下
> db.product_info.insert({productname:'钢笔',price:NumberDecimal("9.999")})
WriteResult({ "nInserted" : 1 })
> db.product_info.find({productname:'钢笔'})
{ "_id" : ObjectId("5e032760c004a56be23e71e9"), "productname" : "钢笔", "price" : NumberDecimal("9.999") }
在使用decimal类型时,要注意精度的问题
案例1
decimal类型 + double类型
> db.product_info.update({productname:'钢笔'},{$inc:{price:3}})
> db.product_info.find({"productname" : "钢笔"})
{ "_id" : ObjectId("5e032760c004a56be23e71e9"), "productname" : "钢笔", "price" : NumberDecimal("12.99900000000000") }
> db.product_info.find({price:{$type:'decimal'}})
{ "_id" : ObjectId("5e032760c004a56be23e71e9"), "productname" : "钢笔", "price" : NumberDecimal("12.99900000000000") }
从上面可以看到decimal+double,price字段虽然类型还是decimal,但是被补了11个0
案例2
decimal类型+decimal类型
> db.product_info.insert({product_name:'car','price':NumberDecimal("200000.2")})
> db.product_info.find({product_name:'car'})
{ "_id" : ObjectId("5e032c72c004a56be23e71ec"), "product_name" : "car", "price" : NumberDecimal("200000.2") }
> db.product_info.update({product_name:'car'},{$inc:{price:NumberDecimal("3000.9")}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.product_info.find({product_name:'car'})
{ "_id" : ObjectId("5e032c72c004a56be23e71ec"), "product_name" : "car", "price" : NumberDecimal("203001.1") }
从上面的结果可以看出,decimal+decimal结果是没问题的
案例3
decimal类型+long类型
> db.product_info.insert({product_name:'bus','price':NumberDecimal("4000000")})
WriteResult({ "nInserted" : 1 })
> db.product_info.find({product_name:'bus'})
{ "_id" : ObjectId("5e032d64c004a56be23e71ed"), "product_name" : "bus", "price" : NumberDecimal("4000000") }
> db.product_info.update({product_name:'bus'},{$inc:{price:NumberLong("20000")}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.product_info.find({product_name:'bus'})
{ "_id" : ObjectId("5e032d64c004a56be23e71ed"), "product_name" : "bus", "price" : NumberDecimal("4020000") }
从上面的结果可以看出,decimal+long结果也是没问题的
案例4
decimal类型+int类型
> db.product_info.find({product_name:'铅笔'})
{ "_id" : ObjectId("5e033af0c004a56be23e71ee"), "product_name" : "铅笔", "price" : NumberDecimal("2.22") }
> db.product_info.update({product_name:'铅笔'},{$inc:{price:NumberInt('2')}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.product_info.find({product_name:'铅笔'})
{ "_id" : ObjectId("5e033af0c004a56be23e71ee"), "product_name" : "铅笔", "price" : NumberDecimal("4.22") }
从上面结果可以看出,decimal+int结果也是没问题的
案例5
decimal类型-decimal类型
> db.test_coll.insert({"number1" : NumberDecimal("364477.2626"),"number2" : NumberDecimal("364476.1626"),'number3':NumberDecimal("32.02")})
WriteResult({ "nInserted" : 1 })
> db.test_coll.find()
{ "_id" : ObjectId("5e033d7cc004a56be23e71ef"), "number1" : NumberDecimal("364477.2626"), "number2" : NumberDecimal("364476.1626"), "number3" : NumberDecimal("32.02") }
相减操作,将number3字段设置为number1和number2的差值
> db.test_coll.find({ "_id" : ObjectId("5e033d7cc004a56be23e71ef")}).forEach(function(item){ item.number = item.number1 - item.number2 ;db.test_coll.save(item) })
> db.test_coll.find()
{ "_id" : ObjectId("5e033d7cc004a56be23e71ef"), "number1" : NumberDecimal("364477.2626"), "number2" : NumberDecimal("364476.1626"), "number3" : NumberDecimal("32.02"), "number" : NaN }
从上面可以看出,decimal类型-decimal类型出现NaN类型,MaN(not a number)属性代表一个不是数字的值,这个跟JavaScript有点像。
案例6
decimal类型+decimal类型
> db.test_coll_info.insert({"number1" : NumberDecimal("364477.2626"),"number2" : NumberDecimal("364476.1626"),'number3':NumberDecimal("32.02")})
WriteResult({ "nInserted" : 1 })
> db.test_coll_info.find()
{ "_id" : ObjectId("5e033f40c004a56be23e71f0"), "number1" : NumberDecimal("364477.2626"), "number2" : NumberDecimal("364476.1626"), "number3" : NumberDecimal("32.02") }
> db.test_coll_info.find({"_id" : ObjectId("5e033f40c004a56be23e71f0")}).forEach(function(item){ item.number = item.number1 + item.number2 ;db.test_coll_info.save(item) })
> db.test_coll_info.find()
{ "_id" : ObjectId("5e033f40c004a56be23e71f0"), "number1" : NumberDecimal("364477.2626"), "number2" : NumberDecimal("364476.1626"), "number3" : NumberDecimal("32.02"), "number" : "NumberDecimal(\"364477.2626\")NumberDecimal(\"364476.1626\")" }
从上面结果可以看出,两个字段结果连接在一起了,这个跟python字符串相加操作是一样的。
提示:对于数字类型的选择和使用,一定要知其然,根据实际场景选择合适的类型,避开盲区。
三、案例讲解
上面只是简单介绍了一下数字的四种类型,并不是本次要讲解的一些重点,由于下面的一些操作需要上面的知识作为基础。对于MySQL DBA来说,对于数字类型会比较了解,就是不同的数字类型对应的值范围是有限制的,否则会导致溢出,从而影响业务。对于MongoDB来说也是一样的,如果数字长度超过了数字类型允许的范围,就会导致数字失真或者报错,这是比较严重的问题。
1、案例1分析
mongos> db.user_info.insert({username:'jack',uid:NumberLong("1206847853192888394")})
WriteResult({ "nInserted" : 1 })
mongos> db.user_info.find({username:'jack'})
{ "_id" : ObjectId("5e045a43d631e2333f8e4c48"), "username" : "jack", "uid" : NumberLong("1206847853192888394") }
从上面结果可以看出uid插入long类型的19位数字是没问题的,接着往下看
mongos> db.user_info.insert({username:'mark',uid:NumberLong("9999999999999999999")})
2019-12-26T15:04:43.631+0800 E QUERY [thread1] Error: could not convert string to long long :
@(shell):1:42
从上面可以看出uid插入的也是19位数字的long但是报错了,数据无法插入,换句话就是说long类型有长度限度。
long类型的范围是-9223372036854775808 ~ 9223372036854775807 , 如果插入的数字不在这范围内,是无法插入,下面我们再次验证下:
mongos> db.user_info.insert({username:'mark',uid:NumberLong("9223372036854775807")})
WriteResult({ "nInserted" : 1 })
mongos> db.user_info.insert({username:'mark',uid:NumberLong("-9223372036854775808")})
WriteResult({ "nInserted" : 1 })
mongos> db.user_info.insert({username:'mark',uid:NumberLong("-9223372036854775809")})
2019-12-26T15:26:06.414+0800 E QUERY [thread1] Error: could not convert string to long long :
@(shell):1:42
上面的结果是:第一条和第二条数据插入成功,第三条数据插入报错。
2、案例2分析
mongos> db.user_test.insert({username:'jack',"category_list" : [
... {
... "category_name" : "管吃",
... "category_id" : NumberLong("1206847853192888320")
... },
... {
... "category_name" : "管喝",
... "category_id" : NumberLong("1206847853192888320")
... }
... ]})
WriteResult({ "nInserted" : 1 })
mongos> db.user_test.find({username:'jack'})
{ "_id" : ObjectId("5e046531d631e2333f8e4c4c"), "username" : "jack", "category_list" : [ { "category_name" : "管吃", "category_id" : NumberLong("1206847853192888320") }, { "category_name" : "管喝", "category_id" : NumberLong("1206847853192888320") } ] }
mongos> db.user_test.update({"_id" : ObjectId("5e046531d631e2333f8e4c4c")},{$set:{'categoryList':[{"category_name":"管车","category_id":NumberLong(1206847853192888393)},{"category_name":"管饱","category_id":NumberLong(1206847853192888394)}]}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
mongos> db.user_test.find({username:'jack'}).pretty()
{
"_id" : ObjectId("5e046531d631e2333f8e4c4c"),
"username" : "jack",
"category_list" : [
{
"category_name" : "管吃",
"category_id" : NumberLong("1206847853192888320")
},
{
"category_name" : "管喝",
"category_id" : NumberLong("1206847853192888320")
}
],
"categoryList" : [
{
"category_name" : "管车",
"category_id" : NumberLong("1206847853192888320")
},
{
"category_name" : "管饱",
"category_id" : NumberLong("1206847853192888320")
}
]
}
从上面的结果,大家发现了吗?category_id字段失真了,如果在生产发生了这种问题,就等着背锅吧!大家继续往下看
mongos> db.user_test.insert({username:'mark',"category_list" : [
... {
... "category_name" : "管吃",
... "category_id" : NumberLong("36363737733")
... }
... ]})
WriteResult({ "nInserted" : 1 })
mongos> db.user_test.update({"_id" : ObjectId("5e046e58d631e2333f8e4c4d")},{$set:{'categoryList':[{"category_name":"管车","category_id":NumberLong("1206847853192888393")}]}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
mongos> db.user_test.find({username:'mark'}).pretty()
{
"_id" : ObjectId("5e046e58d631e2333f8e4c4d"),
"username" : "mark",
"category_list" : [
{
"category_name" : "管吃",
"category_id" : NumberLong("36363737733")
}
],
"categoryList" : [
{
"category_name" : "管车",
"category_id" : NumberLong("1206847853192888393")
}
]
}
大家观察下,有什么不同呢?其实只是category_id字段的NumberLong是否使用了引号包裹数字的区别,文章开始已经说了,NumberLong()函数带的参数是字符串,所以大家一定要注意细节问题,不做背锅侠,或者动不动就说是MongoDB BUG,一定要先从自身找原因。
四、总结
我们在使用不同的数字类型时,首先要清楚知道每种类型的范围,从而才能根据自己的需要来使用。
1、long类型:-9223372036854775808 ~ 9223372036854775807 (922亿亿多)
2、int类型:-2147483648 ~ 2147483647 (21亿多)
3、double类型:-2^1024 ~ +2^1024,也即-1.79E+308 (有这么大)
4、decimal类型:Double类型相当
不同MongoDB版本可能会有微小区别,感兴趣的读者可以自己实验一下,比如int和double类型溢出会如何?可能有惊喜!
五、建议
1、在使用MongoDB时,有些开发比较喜欢用数字类型去自定义_id字段,这个笔者是不建议的,如果真的需要可以新建一个字段,比如uid,尽量不要碰_id默认字段。
2、如果插入的数据超过了数字类型的范围时,那么不要使用数字类型,可以直接保存成字符串存储即可。
扫描二维码
获取更多知识
DBA入坑指南
文章好看点这里