关于MongoDB BSON格式的三个小问题

本文我们从几个问题入手,分析和了解MongoDB的BSON格式的一些特性及使用时的注意事项。

问题1:字符串与数字

注:本问题的之前的讨论本人描述有误,忽略了字符串前面标识长度的4个字节。多谢Zang MingJie在评论中指出。下面内容已经更正。

{a:12}

{a:'12'}

哪个更占空间?

{a:1234}

{a:'1256'}

又是哪个更占空间?

对于第二个例子,应该是后一个更占空间,而对于第一个例子呢,实际还是后一个更占空间。具体原因是:MongoDB对数字的存储分两个级别,一个32位的,一是64位的,如我们上面的例子,都是在32位能表达的范围内,所以数字型的占用4个字节,而字符串型的占用空间为其长度加上最后一位\0结束符,还要加上字符串前面用来标识字符串长度的4个字节,一共是五个字节。所以第一个例子中字符串占用8个字节,第二个例子中字符串占用10个字节。

同样的,在索引里也是一样,不过值得注意的是,字符串与数字在排序方式上不一样,所以在索引时,数字是按数字的方式比对大小,字符串是按字符串的方式比对大小,比如在字符串中,’12′是小于’2′的。

问题2:对象与数组

{a:1,b:2,c:3}

{d:[1,2,3]}

哪个更占空间?

可能你会觉得是第一个,因为它看起来长度更长,或者你还会觉得是因为它包含了每个字段的key值。而实际上呢?

答案是第二个会占用更多空间,因为BSON数据结构在存储数组类型时,是与对象类似的方法,比如第二条的存储实际上相当于这样:

{'d':{'0':1,'1':2,'2':3}}

问题3:_id与$natural的排序

MongoDB中有一个特殊的排序方法,叫$natural,当你指定按$natural排序的时候,相当于是按数据在磁盘上的组织顺序排序。而当你按自动生成的_id字段排序时,相当于是按插入时间排序。当不指定排序顺序的时候,MongoDB是按$natural排序的。这两个有什么差别吗?插入顺序与数据组织顺序不应该一样吗?答案是:有可能。

当MongoDB中的数据有变更时,可能会导致数据的移动,比如你先用下面的方式写入1000条数据:

for(var i=0;i<1000;i++)db.test.insert({_id:i,a:'1'})

然后你分别使用下面命令按_id和$natural排序取出前一条结果:

db.test.find().sort({_id:1}).limit(1)

db.test.find().sort({$natural:1}).limit(1)

你得到的结果会是一样的。

这时候你进行一次update操作,将第一条记录的长度变长:

db.test.update({},{a:'12'})

再通过上面两种不同的排序方式取第一条,你会发现取到的结果还是一样的。

然后你再将第一条记录变长:

db.test.update({},{a:'123'})

再通过上面两种不同的排序方式取第一条,这时候你会看到,通过_id排序查到的还是原来那一条,而通过$natural查到的已经变成了第二条。这是什么原因呢?

当我们执行insert操作的时候,每条记录的长度为31,你可以通过下面命令查看到。

Object.bsonsize(db.test.findOne());

而当我们通过一次update,将a值变为’12′时,其长度变长了1字节,变成了32。当我们又一次将a值变为’123′时,其长度变成了33。那为什么记录长度从31到32的时候,按$natural查到的第一条还是原来的,但是长度从32变到33的时候,通过$natural查到的就变了呢?

先说为什么变了,因为MongoDB在记录长度变化后,发现当前记录所在空间后面没有空余的空间可供其变长。那么这条记录就会被删除然后移动到数据集的最后。这是为什么第二次我们通过$natural查到的第一条不是原来那条的原因。因为原来那条已经跑到最尾巴上去了。

但是为什么第一次增长长度时记录没有移动呢?可能你从数字31,32,33上已经看出点端倪来了。对,就是内存对齐,MongoDB每一条记录都会做4字节的内存对齐。所以在你刚插入的时候,记录长度虽然只有31字节,但是MongoDB会为它分配32字节(8*4)的空间。这时候在其末尾就有一字节的空闲,当你增长一字节的时候,这一字节的空闲正好可以用上。所以就不需要移动位置了。而第二次从32字节变成33字节,原有的空间已经不能装下了,所以会造成数据的移动。实际上4字节对齐,也同时造成一些空间的浪费。同时引起数据变长后移动上的不确定性。

anyShare一切看了好文章不转的行为,都是耍流氓!
          

无觅相关文章插件,快速提升流量

分类 MongoDB · tag ,

  1. 你这篇文章的标题应该叫<>

    $ node
    > var bson = require(“./bson.js”)
    undefined
    > bson.encode({a:12})
    7
    <Buffer 0c 00 00 00 10 61 00 0c 00 00 00 00>
    > bson.encode({a:’12′})
    10
    <Buffer 0f 00 00 00 02 61 00 03 00 00 00 31 32 00 00>
    > bson.encode( {a:1234} )
    7
    <Buffer 0c 00 00 00 10 61 00 d2 04 00 00 00>
    > bson.encode( {a:’1256′} )
    12
    <Buffer 11 00 00 00 02 61 00 05 00 00 00 31 32 35 36 00 00>
    > bson.encode( {a:1,b:2,c:3} )
    21
    <Buffer 1a 00 00 00 10 61 00 01 00 00 00 10 62 00 02 00 00 00 10 63 00 03 00 00 00 00>
    > bson.encode( {d:[1,2,3]} )
    21
    29
    <Buffer 22 00 00 00 04 64 00 1a 00 00 00 10 30 00 01 00 00 00 10 31 00 02 00 00 00 10 32 00 03 00 00 00 00 00>

    • 我使用的是进入mongo客户端后的Object.bsonsize函数来计算bson对象长度。不清楚你的测试结果是使用的哪一个bson库。

      • bson存储一个字符串需要
        n+5 = 长度(4字节) + 字符串(n字节) + (1字节)

        mongo内部, 所有数字都是用long存储, 所以单个数字是8字节

        你比较一下
        {“a”:NumberInt(12)}

        {“a”:”12″}
        的大小试试

        • 首先mongodb内部的数字分三种类型,int32,int64和double(也是64位的)

          然后你这个比对是有问题的,计算{a:1}战用体积的时候,有两种计算情况。一种是它就是整个对象(比如单独计算{a:1}对象的大小)。另一种是它是多个属性中的一个属性(比如计算{b:1}对象变成{b:1,a:1}后增加的长度)。

          比如整个对象就是{a:12},它占用长度应该是12字节,其bson结构如下:

          len|vtype|key|0|v|E
          长度|value类型|key|0|value|结束符号
          4|1|1|1|4|1 = 12字节

          而如果原来是{b:1},变成{b:1,a:12}
          那么增加的部分不需要前面的长度len和后面的结束符号E,它所占用的空间是:

          vtype|key|0|v
          value类型|key|0|value
          1|1|1|4 = 7字节

          而对于字符串型的,比如{a:’12′},它占用长度上只是value做了变化,从4字节变成3字节,也就是:

          len|vtype|key|0|v|E
          长度|value类型|key|0|value|结束符号
          4|1|1|1|3|1 = 11字节

          而如果是原来的{b:1},变成了{b:1,a:’12′}
          那么同理,增加的部分如下:

          vtype|key|0|v
          value类型|key|0|value
          1|1|1|3 = 6字节

          而至于数字是占用4字节还是8字节,这个在不用的客户端lib包中实现不一,如果你直接使用终端计算Object.bsonsize({a:1})的话,数字是存成long型的,占用8字节,整个对象占用应该是一共16字节,如下:

          len|vtype|key|0|v|E
          长度|value类型|key|0|value|结束符号
          4|1|1|1|8|1 = 16字节

          而如果你使用其它客户端,比如PHP的mongodb官方客户端,里面的int是用的int32,也就是4字节的,{a:1}的总占用空间也就是12字节,其结构如下:

          len|vtype|key|0|v|E
          长度|value类型|key|0|value|结束符号
          4|1|1|1|4|1 = 12字节

          我不知道你的测试结果是用什么包做的,不过这个包应该是有问题的。

          • 我用的是mingo 2.0.2

            你错在这些地方:
            1. mongo内部存储数字使用的都是long, 不会用int的
            2. 除去通用的type使用的空间, string需要额外5字节, int, long都不需要额外的空间, 所以 int = 4 < 2字节string = 7 < long = 8

            人人都会犯错误, 希望写文章之前能仔细研究一下, 尽量少犯错

    • 嗯,padding factor的影响是大家都知道的,我这里说的是由于MongoDB内部的内存对齐导致的额外空间消耗,这块在文档里应该没有提及到。

  2. Mongo Shell 支持的javascript语言,js只支持double,但是MongoDB是支持int32,int64和double,所以如果使用其他驱动开发的话,存入的数据数字类型可以是int32,但是在Mongo Shell中都是double型的

  3. 嗯,又仔细看了BSON的实现,确实当时没看全,string类型本身确实会在前面再存储一个4字节的string长度,加上最后一个,一共5字节开销。这个我确实整错了。

    错的最主要的原因是在做测试的时候使用了mongo客户端去计算,mongo客户端使用的是int64类型做表示数字的。8字节大于5+2字节了。

    不过对于第一点我还是有些疑问,你说的是mongo这个环境下,还是说mongodb的存储上是使用的long?如果是mongo这个环境,是没有问题的,而对于mongodb,或者bson来说,是会按数字长度区分int和long来存储的。

    比如用mongo客户端写一个
    db.test.insert({a:1})
    再用PHP客户端写一个
    $db->test->insert(array(‘a’=>1))
    这两个文档find出来都是一样的,但是用Object.bsonsize计算出来的大小是不同的,相差4个字节。