MongoDB索引的使用

系统 1803 0

1 基本索引

在数据库开发中索引是非常重要的,对于检索速度,执行效率有很大的影响。本 文主要描述了MongoDB中索引的使用,以及通过分析执行计划来提高数据库检索 效率。

作为事例,在数据库中插入百万条数据,用于分析

            > 
            
              for
            
             (i = 0; i < 1000000; i++) {
    
            
              "i"
            
                    : i,
    
            
              "username"
            
             : 
            
              "user"
            
             + i,
    
            
              "age"
            
                  : Math.floor(Math.random() * 120),
    
            
              "created"
            
              : 
            
              new
            
            
              Date
            
            ()
}

          

在MongoDB中,所有查询操作,都可以通过执行explain()函数来实现执行的分析, 通过执行查询username为user99999的用户,并执行查询分析,可以得出如下结 果:

            > db.users.find({
            
              "username"
            
            : 
            
              "user99999"
            
            }).explain()
{
        
            
              "cursor"
            
             : 
            
              "BasicCursor"
            
            ,
        
            
              "isMultiKey"
            
             : 
            
              false
            
            ,
        
            
              "n"
            
             : 1,
        
            
              "nscannedObjects"
            
             : 1000000,
        
            
              "nscanned"
            
             : 1000000,
        
            
              "nscannedObjectsAllPlans"
            
             : 1000000,
        
            
              "nscannedAllPlans"
            
             : 1000000,
        
            
              "scanAndOrder"
            
             : 
            
              false
            
            ,
        
            
              "indexOnly"
            
             : 
            
              false
            
            ,
        
            
              "nYields"
            
             : 1,
        
            
              "nChunkSkips"
            
             : 0,
        
            
              "millis"
            
             : 561,
        
            
              "indexBounds"
            
             : {


        },
        
            
              "server"
            
             : 
            
              "WallE.local:27017"
            
            
}

          

其中,“n”表示查找到数据的个数,“nscanedObjects”表示本次查询需要扫描的 对象个数,“milis”表示此次查询耗费的时间,可以看到,这次查询相当于对整 个数据表进行了遍历,共一百万条数据,找到其中一条数据,耗费时间为561毫 秒。

我们也可以使用limit来限制查找的个数,从而提升效率,例如:

            > db.users.find({
            
              "username"
            
            : 
            
              "user99999"
            
            }).limit(1).explain()
{
        
            
              "cursor"
            
             : 
            
              "BasicCursor"
            
            ,
        
            
              "isMultiKey"
            
             : 
            
              false
            
            ,
        
            
              "n"
            
             : 1,
        
            
              "nscannedObjects"
            
             : 100000,
        
            
              "nscanned"
            
             : 100000,
        
            
              "nscannedObjectsAllPlans"
            
             : 100000,
        
            
              "nscannedAllPlans"
            
             : 100000,
        
            
              "scanAndOrder"
            
             : 
            
              false
            
            ,
        
            
              "indexOnly"
            
             : 
            
              false
            
            ,
        
            
              "nYields"
            
             : 0,
        
            
              "nChunkSkips"
            
             : 0,
        
            
              "millis"
            
             : 48,
        
            
              "indexBounds"
            
             : {


        },
        
            
              "server"
            
             : 
            
              "WallE.local:27017"
            
            
}

          

可以看到,这里这次查询只扫描了十万条数据,并且耗费时间大概也只有之前的 十分之一。这是因为,由于限制了本次查询需要获取结果的个数,MongoDB在遍 历数据的过程中一旦发现了找到了结果就直接结束了本次查询,因此效率有了较 大提升。但是这种方式的并不能够解决效率问题,如果需要查询的username为 user999999,那么MongoDB仍然需要遍历整个数据库才能得到结果。

同其他数据库一样,MongoDB也支持索引来提高查询速度,为了提高username的 查询速度,在该字段上建立一个索引:

            > db.users.ensureIndex({
            
              "username"
            
             : 1})

          

执行完该命令后,就在users这个集合中为username新建了一个索引,这个索引 字段可以在db.system.indexes集合中找到:

            > db.system.indexes.find()
{ 
            
              "v"
            
             : 1, 
            
              "key"
            
             : { 
            
              "_id"
            
             : 1 }, 
            
              "ns"
            
             : 
            
              "test.users"
            
            , 
            
              "name"
            
             : 
            
              "_id_"
            
             }
{ 
            
              "v"
            
             : 1, 
            
              "key"
            
             : { 
            
              "username"
            
             : 1 }, 
            
              "ns"
            
             : 
            
              "test.users"
            
            , 
            
              "name"
            
             : 
            
              "username_1"
            
             }

          

值得注意的是,从以上查询中可以看到,每个数据集合都有一个默认的索引字段, 就是_id字段,这个字段在该数据集合建立的时候就会创建。

索引建立之后,再来看下执行效率:

            > db.users.find({
            
              "username"
            
            : 
            
              "user99999"
            
            }).explain()
{
        
            
              "cursor"
            
             : 
            
              "BtreeCursor username_1"
            
            ,
        
            
              "isMultiKey"
            
             : 
            
              false
            
            ,
        
            
              "n"
            
             : 1,
        
            
              "nscannedObjects"
            
             : 1,
        
            
              "nscanned"
            
             : 1,
        
            
              "nscannedObjectsAllPlans"
            
             : 1,
        
            
              "nscannedAllPlans"
            
             : 1,
        
            
              "scanAndOrder"
            
             : 
            
              false
            
            ,
        
            
              "indexOnly"
            
             : 
            
              false
            
            ,
        
            
              "nYields"
            
             : 0,
        
            
              "nChunkSkips"
            
             : 0,
        
            
              "millis"
            
             : 0,
        
            
              "indexBounds"
            
             : {
                
            
              "username"
            
             : [
                        [
                                
            
              "user99999"
            
            ,
                                
            
              "user99999"
            
            
                        ]
                ]
        },
        
            
              "server"
            
             : 
            
              "WallE.local:27017"
            
            
}

          

可以看到,这次MongoDB程序几乎是一瞬间就找到结果,并且扫描的对象个数为1, 可以看到,这次查询直接就找到了需要的结果。

对比第一次没有建立索引时的执行结果,可以看到,第一个字段“cursor”值也有 所变化。作为区分,第一个字段为“BasicCursor”时就表示当前查询没有使用索 引,而建立索引后,该值为“BtreeCursor username_1”,也可以看出来MongoDB 使用的是B树来建立索引。

2 联合索引

通过使用索引,数据库会对数据库中索引中所表示的字段保持已排序状态,也就 是说,我们能够方便的针对该字段进行排序查询如:

            > db.users.find().sort({
            
              "username"
            
             : 1})
...

          

MongoDB能够很快返回结果,但是这种帮助只能在查询字段在首位的情况下才能 生效,如果该字段不在查询的首位,就可能无法使用到该索引带来的好处了,如:

            > db.users.find().sort({
            
              "age"
            
            : 1, 
            
              "username"
            
             : 1})
error: {
        
            
              "$err"
            
             : 
            
              "too much data for sort() with no index.  add an index or specify a smaller limit"
            
            ,
        
            
              "code"
            
             : 10128
}

          

查询字段第一位为“age”,这个时候,MongoDB就会提示错误信息。

为了解决这类问题,MongoDB同其他数据库一样,也提供了联合索引的操作,同 样通过ensureIndex函数来实现:

            > db.users.ensureIndex({
            
              "age"
            
             : 1, 
            
              "username"
            
             : 1})

          

执行这个操作可能需要耗费较长时间,执行成功后,仍然可以通过查询 db.system.indexes集合来查看索引建立情况:

            > db.system.indexes.find()
{ 
            
              "v"
            
             : 1, 
            
              "key"
            
             : { 
            
              "_id"
            
             : 1 }, 
            
              "ns"
            
             : 
            
              "test.users"
            
            , 
            
              "name"
            
             : 
            
              "_id_"
            
             }
{ 
            
              "v"
            
             : 1, 
            
              "key"
            
             : { 
            
              "username"
            
             : 1 }, 
            
              "ns"
            
             : 
            
              "test.users"
            
            , 
            
              "name"
            
             : 
            
              "username_1"
            
             }
{ 
            
              "v"
            
             : 1, 
            
              "key"
            
             : { 
            
              "age"
            
             : 1, 
            
              "username"
            
             : 1 }, 
            
              "ns"
            
             : 
            
              "test.users"
            
            , 
            
              "name"
            
             : 
            
              "age_1_username_1"
            
             }

          

可以看到,刚才的操作建立了一个名字为“age_1_username_1”的联合索引,再次 执行刚才的联合查询,就不会提示出错了。

通过建立该索引,数据库中大致会按照如下方式来保存该索引:

            ...


[26, 
            
              "user1"
            
            ] -> 0x99887766
[26, 
            
              "user2"
            
            ] -> 0x99887722
[26, 
            
              "user5"
            
            ] -> 0x73234234


...


[30, 
            
              "user3"
            
            ] -> 0x37234234
[30, 
            
              "user9"
            
            ] -> 0x33231289


...

          

可以看到,索引中第一个字段“age”按照升序排列进行排序,第二个字段 “username”也在第一个字段的范围内按照升序排列。

在ensureIndex函数中,建立索引时,通过将字段索引置为1,可以将索引标识为 升序排列,如果索引置为-1,则将按照降序排列,如:

            > db.users.ensureIndex({
            
              "age"
            
             : -1, 
            
              "username"
            
             : 1})

          

这样建立的索引“age”字段就将按照降序排列了。

MongoDB如何使用联合索引进行查询,主要是看用户如何执行查询语句,主要有 以下几种情况:

            > db.users.find({
            
              "age"
            
             : 26}).sort({
            
              "username"
            
             : -1})

          

这种情况下,由于查询条件指定了“age”的大小,MongoDB可以使用刚才创建的联 合索引直接找到“age”为26的所有项:

            ...


[26, 
            
              "user1"
            
            ] -> 0x99887766
[26, 
            
              "user2"
            
            ] -> 0x99887722
[26, 
            
              "user5"
            
            ] -> 0x73234234


...

          

并且由于username也是已经排序了的,因此这个查询可以很快完成。这里需要注 意的是,不管创建“username”索引的时候是使用的升序还是降序,MongoDB可以 直接找到最开始或者最后一项,直接进行数据的遍历,因此这个地方创建索引不 会对查询造成影响。

            > db.users.find({
            
              "age"
            
             : {
            
              "$gte"
            
             : 18, 
            
              "lte"
            
             : 30}})

          

这种情况下,MongoDB仍然能够迅速通过联合索引查找到“age”字段在18到30范围 内的所有数据。

最后一种情况较为复杂:

            > db.users.find({
            
              "age"
            
             : {
            
              "$gte"
            
             : 18, 
            
              "lte"
            
             : 30}}).sort({
            
              "username"
            
             : -1})

          

这种情况下,MongoDB首先通过索引查找到“age”范围在18到30之间的所有数据, 由于在这个范围的数据集合中,“username”是未排序的,因此,MongoDB会在内 存中对“username”进行排序,然后将结果输出,如果这个区间中的数据量很大的 话,仍然会出现前面看到的那种一场情况,由于有太多数据需要进行排序操作, 导致程序报错:

            error: {
        
            
              "$err"
            
             : 
            
              "too much data for sort() with no index.  add an index or specify a smaller limit"
            
            ,
        
            
              "code"
            
             : 10128
}

          

这种情况下,可以通过建立一个{"username" : 1, "age" : 1}这样的反向的索 引来帮助进行排序,这个索引建立后,索引大致如下所示:

            ...


[
            
              "user0"
            
            , 69]
[
            
              "user1"
            
            , 50]
[
            
              "user10"
            
            , 80]
[
            
              "user100"
            
            , 48]
[
            
              "user1000"
            
            , 111]
[
            
              "user10000"
            
            , 98]
[
            
              "user100000"
            
            , 21] -> 0x73f0b48d
[
            
              "user100001"
            
            , 60]
[
            
              "user100002"
            
            , 82]
[
            
              "user100003"
            
            , 27] -> 0x0078f55f
[
            
              "user100004"
            
            , 22] -> 0x5f0d3088
[
            
              "user100005"
            
            , 95]

...

          

这样,MongoDB可以通过遍历一次这个索引列表来进行排序操作。这样也避免了 在内存中进行大数据的排序操作。

对刚才的查询执行查询计划可以看到:

            > db.users.find({
            
              "age"
            
             : {
            
              "$gte"
            
             : 21, 
            
              "$lte"
            
             : 30}}).sort({
            
              "username"
            
             : 1}).explain()
{
        
            
              "cursor"
            
             : 
            
              "BtreeCursor username_1"
            
            ,
        
            
              "isMultiKey"
            
             : 
            
              false
            
            ,
        
            
              "n"
            
             : 83417,
        
            
              "nscannedObjects"
            
             : 1000000,
        
            
              "nscanned"
            
             : 1000000,
        
            
              "nscannedObjectsAllPlans"
            
             : 1002214,
        
            
              "nscannedAllPlans"
            
             : 1002214,
        
            
              "scanAndOrder"
            
             : 
            
              false
            
            ,
        
            
              "indexOnly"
            
             : 
            
              false
            
            ,
        
            
              "nYields"
            
             : 1,
        
            
              "nChunkSkips"
            
             : 0,
        
            
              "millis"
            
             : 1923,
        
            
              "indexBounds"
            
             : {
                
            
              "username"
            
             : [
                        [
                                {
                                        
            
              "$minElement"
            
             : 1
                                },
                                {
                                        
            
              "$maxElement"
            
             : 1
                                }
                        ]
                ]
        },
        
            
              "server"
            
             : 
            
              "WallE.local:27017"
            
            
}

          

使用hint函数,使用反向索引之后的结果如下:

            > db.users.find({
            
              "age"
            
             : {
            
              "$gte"
            
             : 21, 
            
              "$lte"
            
             : 30}}).sort({
            
              "username"
            
             : 1}).hint({
            
              "username"
            
             : 1, 
            
              "age"
            
             : 1}).explain()
{
        
            
              "cursor"
            
             : 
            
              "BtreeCursor username_1_age_1"
            
            ,
        
            
              "isMultiKey"
            
             : 
            
              false
            
            ,
        
            
              "n"
            
             : 83417,
        
            
              "nscannedObjects"
            
             : 83417,
        
            
              "nscanned"
            
             : 984275,
        
            
              "nscannedObjectsAllPlans"
            
             : 83417,
        
            
              "nscannedAllPlans"
            
             : 984275,
        
            
              "scanAndOrder"
            
             : 
            
              false
            
            ,
        
            
              "indexOnly"
            
             : 
            
              false
            
            ,
        
            
              "nYields"
            
             : 2,
        
            
              "nChunkSkips"
            
             : 0,
        
            
              "millis"
            
             : 3064,
        
            
              "indexBounds"
            
             : {
                
            
              "username"
            
             : [
                        [
                                {
                                        
            
              "$minElement"
            
             : 1
                                },
                                {
                                        
            
              "$maxElement"
            
             : 1
                                }
                        ]
                ],
                
            
              "age"
            
             : [
                        [
                                21,
                                30
                        ]
                ]
        },
        
            
              "server"
            
             : 
            
              "WallE.local:27017"
            
            
}

          

可以看到,第二次执行的时间似乎还要长一些。因此上面介绍的理论并不一定有 效,很多时候,为了提高数据库的查询效率,最好对所有查询语句执行查询计划, 查看执行差异,从而进行优化。

通过上面的例子可以看到在使用联合索引的时候,进行查询操作时,排在前面的 字段如果按照联合索引的字段进行查询,都能够利用到联合索引的优点。

例如,执行如下查询时,“age”字段是{"age" : 1, "username" : 1}的第一个字 段,这个时候就可以使用到这个联合索引进行查询。

            > db.users.find({
            
              "age"
            
             : 99})

          

例如查询:

            > db.users.find({
            
              "a"
            
             : 10, 
            
              "b"
            
             : 20, 
            
              "c"
            
             : 30})

          

就可以使用索引:{"a" : 1, "b" : 1, "c" : 1, "d" : 1},只要是按照顺序的 查询都可以利用到索引来进行查询,当然,如果顺序不一致,就无法使用到索引 了,例如:

            > db.users.find({
            
              "c"
            
             : 20, 
            
              "a"
            
             : 10})

          

就无法使用{"a" : 1, "b" : 1, "c" : 1, "d" : 1}索引带来的好处了。

同关系型数据库一致,在MongoDB执行查询操作时,把最容易进行范围限定的条 件放到最前面,是最有利于查询操作的,排在前面的条件能够筛选的出来的结果 越少,后续的查询效率也就越高。

在MongoDB中,对查询优化采用这样一种方式,当查询条件与索引字段完全一致 时(如查询“i”的字段,同时也存在一个索引为“i”的字段),则MongoDB会直接 使用这个索引进行查询。反之,如果有多个索引可能作用于此次查询,则 MongoDB会采用不同的索引同时并行执行多个查询操作,最先返回100个数据的查 询将会继续进行查询,剩余的查询操作将会被终止。MongoDB会将此次查询进行 缓存,下次查询会继续使用,直到对该数据集进行了一定修改后,再次采用这种 方式进行更新。在执行explain()函数后输出字段中的“allPlans”就表示,所有 尝试进行的查询操作次数。

3 索引类型

在MongoDB中,也可以建立唯一索引:

            > db.users.ensureIndex({
            
              "username"
            
             : 1}, {
            
              "unique"
            
             : 
            
              true
            
            })

          

建立了唯一索引后,如果插入相同名称的数据,系统就会报错:

            > db.users.insert({
            
              "username"
            
             : 
            
              "user1"
            
            })
E11000 duplicate key error index: test.users.$username_1  dup key: { : 
            
              "user1"
            
             }

          

同样的,联合索引也可以建立唯一索引:

            > db.users.ensureIndex({
            
              "age"
            
             : 1, 
            
              "username"
            
             : 1}, {
            
              "unique"
            
             : 
            
              true
            
            })

          

创建成功后,如果插入相同的数据内容同样会报错。

如果数据库中已经包含了重复数据,可以通过创建唯一索引的方式来进行删除。 但是注意,这种方式非常危险,如果不是确定数据无效,不能这样操作,因为, MongoDB只会保留遇到的第一个不同的数据项,后续重复数据都将被删除:

            > db.users.ensureIndex({
            
              "age"
            
             : 1, 
            
              "username"
            
             : 1}, {
            
              "unique"
            
             : 
            
              true
            
            , 
            
              "dropDups"
            
             : 
            
              true
            
            })

          

某些时候,我们希望对数据库中某个字段建立唯一索引,但是又不一定是每条数 据都包含这个字段,这个时候,可以使用sparse索引来解决这个问题:

            > db.users.ensureIndex({
            
              "email"
            
             : 1}, {
            
              "unique"
            
             : 
            
              true
            
            , 
            
              "sparse"
            
             : 1})

          

如果存在如下数据:

            > db.foo.find()
{ 
            
              "_id"
            
             : 0 }
{ 
            
              "_id"
            
             : 1, 
            
              "x"
            
             : 1 }
{ 
            
              "_id"
            
             : 2, 
            
              "x"
            
             : 2 }
{ 
            
              "_id"
            
             : 3, 
            
              "x"
            
             : 3 }

          

当没有建立索引的情况下,执行如下操作会返回:

            > db.foo.find({
            
              "x"
            
             : {
            
              "$ne"
            
             : 2}})
{ 
            
              "_id"
            
             : 0 }
{ 
            
              "_id"
            
             : 1, 
            
              "x"
            
             : 1 }
{ 
            
              "_id"
            
             : 3, 
            
              "x"
            
             : 3 }

          

如果建立了sparse索引,则MongoDB就不会返回第一条数据,而是返回所有包含 “x”字段的数据:

            > db.foo.find({
            
              "x"
            
             : {
            
              "$ne"
            
             : 2}})
{ 
            
              "_id"
            
             : 0 }
{ 
            
              "_id"
            
             : 1, 
            
              "x"
            
             : 1 }
{ 
            
              "_id"
            
             : 3, 
            
              "x"
            
             : 3 }

          

4 索引管理

通过执行getIndexes()函数,可以获得当前数据集中所有的索引:

            > db.users.getIndexes()
[
        {
                
            
              "v"
            
             : 1,
                
            
              "key"
            
             : {
                        
            
              "_id"
            
             : 1
                },
                
            
              "ns"
            
             : 
            
              "test.users"
            
            ,
                
            
              "name"
            
             : 
            
              "_id_"
            
            
        },
        {
                
            
              "v"
            
             : 1,
                
            
              "key"
            
             : {
                        
            
              "age"
            
             : 1,
                        
            
              "username"
            
             : 1
                },
                
            
              "ns"
            
             : 
            
              "test.users"
            
            ,
                
            
              "name"
            
             : 
            
              "age_1_username_1"
            
            
        },
        {
                
            
              "v"
            
             : 1,
                
            
              "key"
            
             : {
                        
            
              "username"
            
             : 1,
                        
            
              "age"
            
             : 1
                },
                
            
              "ns"
            
             : 
            
              "test.users"
            
            ,
                
            
              "name"
            
             : 
            
              "username_1_age_1"
            
            
        },
        {
                
            
              "v"
            
             : 1,
                
            
              "key"
            
             : {
                        
            
              "username"
            
             : 1
                },
                
            
              "unique"
            
             : 
            
              true
            
            ,
                
            
              "ns"
            
             : 
            
              "test.users"
            
            ,
                
            
              "name"
            
             : 
            
              "username_1"
            
            
        }
]

          

其中的“name”字段可以用于对索引的删除操作:

            > db.users.dropIndex(
            
              "username_1_age_1"
            
            )

          

就将删除{"username" : 1, "age" : 1}这个索引。

 

Author: Chenbin

Created: 2013-10-26 Sat 14:12

Emacs 24.3.1 ( Org mode 8.2.1)

Validate

MongoDB索引的使用


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论