暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MongoDB数字类型你真的了解吗?

DBA入坑指南 2021-04-18
7048


点击上方蓝字关注我们


一、前言






最近刚好有开发在使用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、如果插入的数据超过了数字类型的范围时,那么不要使用数字类型,可以直接保存成字符串存储即可。


                                        ☆ END ☆



                                        扫描二维码

                                        获取更多知识

                                        DBA入坑指南



                                        文章好看点这里

                                        文章转载自DBA入坑指南,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                        评论