MongoDB索引的使用

1 基本索引

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

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

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

在MongoDB中,全部查询操做,均可以经过执行explain()函数来实现执行的分析, 经过执行查询username为user99999的用户,并执行查询分析,能够得出以下结 果:java

> 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毫 秒。git

咱们也可使用limit来限制查找的个数,从而提高效率,例如:github

> 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

同其余数据库同样,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字段,这个字段在该数据集合创建的时候就会建立。dom

索引创建以后,再来看下执行效率:函数

> 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

相关文章
相关标签/搜索