MongoDB指南---十、索引、复合索引 简介

上一篇文章: MongoDB指南---九、游标与数据库命令
下一篇文章: MongoDB指南---十一、使用复合索引、$操做符如何使用索引、索引对象和数组、索引基数

本章介绍MongoDB的索引,索引能够用来优化查询,并且在某些特定类型的查询中,索引是必不可少的。面试

  • 什么是索引?为何要用索引?
  • 如何选择须要创建索引的字段?
  • 如何强制使用索引?如何评估索引的效率?
  • 建立索引和删除索引。

为集合选择合适的索引是提升性能的关键。shell

一、 索引简介

数据库索引与书籍的索引相似。有了索引就不须要翻整本书,数据库能够直接在索引中查找,在索引中找到条目之后,就能够直接跳转到目标文档的位置,这能使查找速度提升几个数量级。
不使用索引的查询称为全表扫描(这个术语来自关系型数据库),也就是说,服务器必须查找完一整本书才能找到查询结果。这个处理过程与咱们在一本没有索引的书中查找信息很像:从第1页开始一直读完整本书。一般来讲,应该尽可能避免全表扫描,由于对于大集合来讲,全表扫描的效率很是低。
来看一个例子,咱们建立了一个拥有1 000 000个文档的集合(若是你想要10 000 000或者100 000 000个文档也行,只要你有那个耐心):数据库

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

若是在这个集合上作查询,可使用explain()函数查看MongoDB在执行查询的过程当中所作的事情。下面试着查询一个随机的用户名:segmentfault

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

    }
}

5.2节会详细介绍输出信息里的这些字段,目前来讲能够忽略大多数字段。"nscanned"是MongoDB在完成这个查询的过程当中扫描的文档总数。能够看到,这个集合中的每一个文档都被扫描过了。也就是说,为了完成这个查询,MongoDB查看了每个文档中的每个字段。这个查询耗费了将近1秒的时间才完成:"millis"字段显示的是这个查询耗费的毫秒数。
字段"n"显示了查询结果的数量,这里是1,由于这个集合中确实只有一个username为"user101"的文档。注意,因为不知道集合里的username字段是惟一的,MongoDB不得不查看集合中的每个文档。为了优化查询,将查询结果限制为1,这样MongoDB在找到一个文档以后就会中止了:数组

> db.users.find({username: "user101"}).limit(1).explain()
{
    "cursor" : "BasicCursor",
    "nscanned" : 102,
    "nscannedObjects" : 102,
    "n" : 1,
    "millis" : 2,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {

    }
}

如今,所扫描的文档数量极大地减小了,并且整个查询几乎是瞬间完成的。可是,这个方案是不现实的:若是要查找的是user999999呢?咱们仍然不得不遍历整个集合,并且,随着用户的增长,查询会愈来愈慢。
对于此类查询,索引是一个很是好的解决方案:索引能够根据给定的字段组织数据,让MongoDB可以很是快地找到目标文档。下面尝试在username字段上建立一个索引:服务器

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

因为机器性能和集合大小的不一样,建立索引有可能须要花几分钟时间。若是对ensureIndex的调用没能在几秒钟后返回,能够在另外一个shell中执行db.currentOp()或者是检查mongod的日志来查看索引建立的进度。
索引建立完成以后,再次执行最初的查询:dom

> db.users.find({"username" : "user101"}).explain()
{
    "cursor" : "BtreeCursor username_1",
    "nscanned" : 1,
    "nscannedObjects" : 1,
    "n" : 1,
    "millis" : 3,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {
        "username" : [
            [
                "user101",
                "user101"
            ]
        ]
    }
}

此次explain()的输出内容比以前复杂一些,可是目前咱们只须要注意"n"、"nscanned"和"millis"这几个字段,能够忽略其余字段。能够看到,这个查询如今几乎是瞬间完成的(甚至能够更好),并且对于任意username的查询,所耗费的时间基本一致:函数

> db.users.find({username: "user999999"}).explain().millis
1

能够看到,使用了索引的查询几乎能够瞬间完成,这是很是激动人心的。然而,使用索引是有代价的:对于添加的每个索引,每次写操做(插入、更新、删除)都将耗费更多的时间。这是由于,当数据发生变更时,MongoDB不只要更新文档,还要更新集合上的全部索引。所以,MongoDB限制每一个集合上最多只能有64个索引。一般,在一个特定的集合上,不该该拥有两个以上的索引。因而,挑选合适的字段创建索引很是重要。性能

MongoDB的索引几乎与传统的关系型数据库索引如出一辙,因此若是已经掌握了那些技巧,则能够跳过本节的语法说明。后面会介绍一些索引的基础知识,但必定要记住这里涉及的只是冰山一角。绝大多数优化MySQL/Oracle/SQLite索引的技巧一样也适用于MongoDB(包括“Use the Index, Luke”上的教程 http://use-the-index-luke.com)。

为了选择合适的键来创建索引,能够查看经常使用的查询,以及那些须要被优化的查询,从中找出一组经常使用的键。例如,在上面的例子中,查询是在"username"上进行的。若是这是一个很是通用的查询,或者这个查询形成了性能瓶颈,那么在"username"上创建索引会是很是好的选择。然而,若是这只是一个不多用到的查询,或者只是给管理员用的查询(管理员并不须要太在乎查询耗费的时间),那就不该该对"username"创建索引。优化

二、 复合索引简介

索引的值是按必定顺序排列的,所以,使用索引键对文档进行排序很是快。然而,只有在首先使用索引键进行排序时,索引才有用。例如,在下面的排序里,"username"上的索引没什么做用:

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

这里先根据"age"排序再根据"username"排序,因此"username"在这里发挥的做用并不大。为了优化这个排序,可能须要在"age"和"username"上创建索引:

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

这样就创建了一个复合索引(compound index)。若是查询中有多个排序方向或者查询条件中有多个键,这个索引就会很是有用。复合索引就是一个创建在多个字段上的索引。
假如咱们有一个users集合(以下所示),若是在这个集合上执行一个不排序(称为天然顺序)的查询:

> db.users.find({}, {"_id" : 0, "i" : 0, "created" : 0})
{ "username" : "user0", "age" : 69 }
{ "username" : "user1", "age" : 50 }
{ "username" : "user2", "age" : 88 }
{ "username" : "user3", "age" : 52 }
{ "username" : "user4", "age" : 74 }
{ "username" : "user5", "age" : 104 }
{ "username" : "user6", "age" : 59 }
{ "username" : "user7", "age" : 102 }
{ "username" : "user8", "age" : 94 }
{ "username" : "user9", "age" : 7 }
{ "username" : "user10", "age" : 80 }
...

若是使用{"age" : 1, "username" : 1}创建索引,这个索引大体会是这个样子:

[0, "user100309"] -> 0x0c965148
[0, "user100334"] -> 0xf51f818e
[0, "user100479"] -> 0x00fd7934
...
[0, "user99985" ] -> 0xd246648f
[1, "user100156"] -> 0xf78d5bdd
[1, "user100187"] -> 0x68ab28bd
[1, "user100192"] -> 0x5c7fb621
...
[1, "user999920"] -> 0x67ded4b7
[2, "user100141"] -> 0x3996dd46
[2, "user100149"] -> 0xfce68412
[2, "user100223"] -> 0x91106e23
...

每个索引条目都包含一个"age"字段和一个"username"字段,而且指向文档在磁盘上的存储位置(这里使用十六进制数字表示,能够忽略)。注意,这里的"age"字段是严格升序排列的,"age"相同的条目按照"username"升序排列。每一个"age"都有大约8000个对应的"username",这里只是挑选了少许数据用于传达大概的信息。
MongoDB对这个索引的使用方式取决于查询的类型。下面是三种主要的方式。

  • db.users.find({"age" : 21}).sort({"username" : -1})

这是一个点查询(point query),用于查找单个值(尽管包含这个值的文档可能有多个)。因为索引中的第二个字段,查询结果已是有序的了:MongoDB能够从{"age" : 21}匹配的最后一个索引开始,逆序依次遍历索引:

[21, "user999977"] -> 0x9b3160cf
[21, "user999954"] -> 0xfe039231
[21, "user999902"] -> 0x719996aa
...

这种类型的查询是很是高效的:MongoDB可以直接定位到正确的年龄,并且不须要对结果进行排序(由于只须要对数据进行逆序遍历就能够获得正确的顺序了)。
注意,排序方向并不重要:MongoDB能够在任意方向上对索引进行遍历。

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

这是一个多值查询(multi-value query),查找到多个值相匹配的文档(在本例中,年龄必须介于21到30之间)。MongoDB会使用索引中的第一个键"age"获得匹配的文档,以下所示:

[21, "user100000"] -> 0x37555a81
[21, "user100069"] -> 0x6951d16f
[21, "user1001"] ->   0x9a1f5e0c
[21, "user100253"] -> 0xd54bd959
[21, "user100409"] -> 0x824fef6c
[21, "user100469"] -> 0x5fba778b
...
[30, "user999775"] -> 0x45182d8c
[30, "user999850"] -> 0x1df279e9
[30, "user999936"] -> 0x525caa57

一般来讲,若是MongoDB使用索引进行查询,那么查询结果文档一般是按照索引顺序排列的。

  • db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username":1})

这是一个多值查询,与上一个相似,只是此次须要对查询结果进行排序。跟以前同样,MongoDB会使用索引来匹配查询条件:

[21, "user100000"] -> 0x37555a81
[21, "user100069"] -> 0x6951d16f
[21, "user1001"] ->   0x9a1f5e0c 
[21, "user100253"] -> 0xd54bd959
...
[22, "user100004"] -> 0x81e862c5
[22, "user100328"] -> 0x83376384
[22, "user100335"] -> 0x55932943
[22, "user100405"] -> 0x20e7e664
...

然而,使用这个索引获得的结果集中"username"是无序的,而查询要求结果以"username"升序排列,因此MongoDB须要先在内存中对结果进行排序,而后才能返回。所以,这个查询一般不如上一个高效。
固然,查询速度取决于有多少个文档与查询条件匹配:若是结果集中只有少数几个文档,MongoDB对这些文档进行排序并不须要耗费多少时间。若是结果集中的文档数量比较多,查询速度就会比较慢,甚至根本不能用:若是结果集的大小超过32 MB,MongoDB就会出错,拒绝对如此多的数据进行排序:

Mon Oct 29 16:25:26 uncaught exception: error: {
    "$err" : "too much data for sort() with no index. add an index or
        specify a smaller limit",
    "code" : 10128
}

最后一个例子中,还可使用另外一个索引(一样的键,可是顺序调换了):{"username" : 1, "age" : 1}。MongoDB会反转全部的索引条目,可是会以你指望的顺序返回。MongoDB会根据索引中的"age"部分挑选出匹配的文档:

["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不得不扫描整个索引以便找到全部匹配的文档。所以,若是对查询结果的范围作了限制,那么MongoDB在几回匹配以后就能够再也不扫描索引,在这种状况下,将排序键放在第一位是一个很是好的策略。
能够经过explain()来查看MongoDB对db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username" : 1})的默认行为:

> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).
... sort({"username" : 1}).
... explain()
{
    "cursor" : "BtreeCursor age_1_username_1",
    "isMultiKey" : false,
    "n" : 83484,
    "nscannedObjects" : 83484,
    "nscanned" : 83484,
    "nscannedObjectsAllPlans" : 83484,
    "nscannedAllPlans" : 83484,
    "scanAndOrder" : true,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 2766,
    "indexBounds" : {
        "age" : [
            [
                21,
                30
            ]
        ],
        "username" : [
            [
                {
                    "$minElement" : 1
                },
                {
                    "$maxElement" : 1
                }
            ]
        ]
    },
    "server" : "spock:27017"
}

能够忽略大部分字段,后面会有相关介绍。注意,"cursor"字段说明此次查询使用的索引是 {"age" : 1, "user name" : 1},并且只查找了不到1/10的文档("nscanned"只有83484),可是这个查询耗费了差很少3秒的时间("millis"字段显示的是毫秒数)。这里的"scanAndOrder"字段的值是true:说明MongoDB必须在内存中对数据进行排序,如以前所述。
能够经过hint来强制MongoDB使用某个特定的索引,再次执行这个查询,可是此次使用{"username" : 1, "age" : 1}做为索引。这个查询扫描的文档比较多,可是不须要在内存中对数据排序:

> 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" : 83484,
    "nscannedObjects" : 83484,
    "nscanned" : 984434,
    "nscannedObjectsAllPlans" : 83484,
    "nscannedAllPlans" : 984434,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 14820,
    "indexBounds" : {
        "username" : [
            [
                {
                    "$minElement" : 1
                },
                {
                    "$maxElement" : 1
                }
            ]
        ],
        "age" : [
            [
                21,
                30
            ]
        ]
    },
    "server" : "spock:27017"
}

注意,此次查询耗费了将近15秒才完成。对比鲜明,第一个索引速度更快。然而,若是限制每次查询的结果数量,新的赢家产生了:

> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).
... sort({"username" : 1}).
... limit(1000).
... hint({"age" : 1, "username" : 1}).
... explain()['millis']
2031
> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).
... sort({"username" : 1}).
... limit(1000).
... hint({"username" : 1, "age" : 1}).
... explain()['millis']
181

第一个查询耗费的时间仍然介于2秒到3秒之间,可是第二个查询只用了不到1/5秒!所以,应该就在应用程序使用的查询上执行explain()。排除掉那些可能会致使explain()输出信息不许确的选项。
在实际的应用程序中,{"sortKey" : 1, "queryCriteria" : 1}索引一般是颇有用的,由于大多数应用程序在一次查询中只须要获得查询结果最前面的少数结果,而不是全部可能的结果。并且,因为索引在内部的组织形式,这种方式很是易于扩展。索引本质上是树,最小的值在最左边的叶子上,最大的值在最右边的叶子上。若是有一个日期类型的"sortKey"(或是其余可以随时间增长的值),当从左向右遍历这棵树时,你实际上也花费了时间。所以,若是应用程序须要使用最近数据的机会多于较老的数据,那么MongoDB只需在内存中保留这棵树最右侧的分支(最近的数据),而没必要将整棵树留在内存中。相似这样的索引是右平衡的(right balanced),应该尽量让索引是右平衡的。"_id"索引就是一个典型的右平衡索引。

上一篇文章: MongoDB指南---九、游标与数据库命令
下一篇文章: MongoDB指南---十一、使用复合索引、$操做符如何使用索引、索引对象和数组、索引基数
相关文章
相关标签/搜索