索引就是用来加速查询的。MongoDB就是使用ensureIndex来创建索引,创建索引后,MongoDB组织集合的方式会发生变化:
> db.c.find().limit(3) { "_id" : ObjectId("55079008f6ee9030e868d86e"), "x" : 0 } { "_id" : ObjectId("55079008f6ee9030e868d86f"), "x" : 1 } { "_id" : ObjectId("55079008f6ee9030e868d870"), "x" : 2 }当我们执行索引后(-1代表降序,1代表升序):
> db.c.ensureIndex({"x" : -1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 2, "numIndexesAfter" : 2, "note" : "all indexes already exist", "ok" : 1 }集合内部的组织变成了:
{ "_id" : ObjectId("55079008f6ee9030e868d86e"), "x" : 2 } { "_id" : ObjectId("55079008f6ee9030e868d86f"), "x" : 1 } { "_id" : ObjectId("55079008f6ee9030e868d870"), "x" : 0 }但是,它们显示却没有多少改变:
> db.c.find().limit(3) { "_id" : ObjectId("55079008f6ee9030e868d86e"), "x" : 0 } { "_id" : ObjectId("55079008f6ee9030e868d86f"), "x" : 1 } { "_id" : ObjectId("55079008f6ee9030e868d870"), "x" : 2 }
索引应该避免关联过于频繁的键值。考虑我们有一个集合,保存了用户的状态信息。现在想要查询用户和日期,取出某一个用户最近的状态,则我们可以这样创建索引:
> db.status.ensureIndex({"user" : 1, "date" : -1})但是这种扩展索引的方法有一个很大的弊端是:假设有数百万的用户,没人每天都有数十条状态的更新,那么这个索引反而是一个累赘,因为它是先用user来建立索引的。
我们应该修改为:
> db.status.ensureIndex({"date" : -1, "user" : 1})所以,建立索引时要考虑如下问题:
1. 会做什么样的查询?其中哪些键需要索引?
2. 每个键的索引方向是怎样的?
3. 如何应对扩展?有没有不同的键的排列可以使常用数据更多的保留在内存中。
为内嵌文档的键建立索引和为普通的键创建索引没有什么区别:
> db.blog.ensureIndex({"comments.date" : 1}) { "createdCollectionAutomatically" : true, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 }
随着集合的增长,需要针对查询中大量的排序做索引。如果对没有索引的键调用sort,MongoDB需要将所有数据提取到内存来排序。因此,可以做无索引排序是有一个上限的,那就是不可能在内存里面做T级别数据的排序。一旦集合大到不能在内存中排序,MongoDB就会报错。
按照排序来索引以便让MongoDB按照顺序提取数据,这样就能排序大规模数据,而不必担心用光内存。
唯一索引可以确保集合的每一个文档的指定键都有唯一值:
> db.people.ensureIndex({"word" : 1}, {"unique" : true}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 3, "numIndexesAfter" : 4, "ok" : 1 } > db.people.find() { "_id" : ObjectId("5507c134c5bce3f400b5c787"), "word" : "a", "num" : 1 } { "_id" : ObjectId("5507c13ac5bce3f400b5c788"), "word" : "b", "num" : 3 } { "_id" : ObjectId("5507c141c5bce3f400b5c789"), "word" : "c", "num" : 5 } { "_id" : ObjectId("5507c148c5bce3f400b5c78a"), "word" : "d", "num" : 2 } { "_id" : ObjectId("5507c14fc5bce3f400b5c78b"), "word" : "e", "num" : 4 } > db.people.insert({"word" : "a", "num" : 10}) WriteResult({ "nInserted" : 0, "writeError" : { "code" : 11000, "errmsg" : "E11000 duplicate key error index: mydb.people.$word_1 dup key: { : \"a\" }" } })
如果已有的集合中存在重复,我们需要dropDups选项保留发现的第一个文档,而删除接下来的重复值的文档:
> db.a.find() { "_id" : ObjectId("5507c94acdf38f5d77984d05"), "x" : 1 } { "_id" : ObjectId("5507c951cdf38f5d77984d06"), "x" : 1 } { "_id" : ObjectId("5507c964cdf38f5d77984d07"), "x" : 2 } > db.a.ensureIndex({x : 1}, {unique : 1, dropDups : 1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "errmsg" : "exception: E11000 duplicate key error index: mydb.a.$x_1 dup key: { : 1.0 }", "code" : 11000, "ok" : 0 }但是我使用的时候,却报错,不知道为什么。
explain是一个非常有用的工具,会帮助你获得查询方面诸多有用的信息。只要对游标调用该方法,就可以得到查询细节。explain会返回一个文档,而不是游标本身,这是与多数游标方法不同之处。
> db.foo.find() { "_id" : ObjectId("55078dbff6ee9030e868d86c"), "apple" : 1, "banana" : 6, "peach" : 3 } { "_id" : ObjectId("55078dd4f6ee9030e868d86d"), "apple" : 8, "spinach" : 4, "watermelon" : 4 } > db.foo.find().explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "mydb.foo", "indexFilterSet" : false, "parsedQuery" : { "$and" : [ ] }, "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "$and" : [ ] }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "leichaojian-ThinkPad-T430", "port" : 27017, "version" : "3.0.1", "gitVersion" : "534b5a3f9d10f00cd27737fbcd951032248b5952" }, "ok" : 1 }explain会返回查询使用的索引情况,耗时及扫描文档数的统计信息。
备注: 书上的例子和我所使用的MongoDB版本3.0.1的输出有很大的出入,所以不做笔记。
索引的元信息存储在每一个数据库的system.indexes集合中。这是一个保留集合,不能对其插入或者删除文档。操作只能通过ensureIndex或者dropIndexes进行。