MongoDB的学习--索引
索引可以用来优化查询,而且在某些特定类型的查询中,索引是必不可少的。为集合选择合适的索引是提高性能的关键。
先来mock数据
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在执行查询的过程中所做的事情。执行如下命令,查找用户名为user1000的用户。
db.users.find({username:"user1000"}).explain()
得到结果如下:
{ "cursor" : "BasicCursor", "isMultiKey" : false, "n" : 1, "nscannedObjects" : 1000000, "nscanned" : 1000000, "nscannedObjectsAllPlans" : 1000000, "nscannedAllPlans" : 1000000, "scanAndOrder" : false, "indexOnly" : false, "nYields" : 7813, "nChunkSkips" : 0, "millis" : 411, "server" : "user:27017", "filterSet" : false }
之后会详细介绍各个字段的意思,现在我们只需要知道,"n"表示查询结果的数量,"nscanned"表示MongoDB在完成这个查询的过程中扫描的文件总数,"millis"表示这个查询耗费的毫秒数。可以看到,为了查找user1000,MongoDB遍历了整个集合,消耗了411毫秒。
为了优化查询,我们可以在查找到一个结果的时候,就结束查询,返回结果。命令如下:
db.users.find({username:"user1000"}).limit(1).explain()
结果如下:
{ "cursor" : "BasicCursor", "isMultiKey" : false, "n" : 1, "nscannedObjects" : 1001, "nscanned" : 1001, "nscannedObjectsAllPlans" : 1001, "nscannedAllPlans" : 1001, "scanAndOrder" : false, "indexOnly" : false, "nYields" : 7, "nChunkSkips" : 0, "millis" : 1, "server" : "user:27017", "filterSet" : false }
可以看到扫描文档数和消耗时间都变少了很多,但是如果我们要查找user999999,MongoDB还是要遍历集合才能找到。而且随着用户数量的增多,查询会越来越慢。
对于这种情况,创建索引是一个非常好的解决方案:索引可以根据给定的字段组织数据,让MongoDB能够非常快速的找到目标文档。使用如下命令,在username字段上创建一个索引。
db.users.ensureIndex({"username":1})
然后再来执行一下之前执行过的语句
db.users.find({username:"user1000"}).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" : [ [ "user1000", "user1000" ] ] }, "server" : "user:27017", "filterSet" : false }
然后你会发现查询变快了很多,几乎是瞬间完成,这就是使用索引的效果。但是索引也是有代价的,对于添加的每一个索引,每次写操作(插入、更新、删除)都将耗费更多的时间。这是因为当数据发生变动时,MongoDB不仅要更新文档,还要更新集合上的所有索引。因此,MongoDB限制每个集合上最多只能有64个索引。通常,在一个特定的集合上,不应该拥有两个以上的索引。
当一个索引建立在多个字段上时,我们称它为复合索引,创建的语句如下:
db.users.ensureIndex({"age":1, "username":1})
如果查询中有多个排序方向或者查询条件中有多个键,复合索引就会非常有用。
MongoDB对这个索引的使用方法取决于查询的类型。下面是三种主要的方式。
第一种:
db.users.find({"age":21}).sort({"username":-1})
这是一个点查询,用于查找单个值(尽管包含这个值的文档是多个)。由于索引中的第二个字段,查询结果已经是有序的了。这种类型的查询是非常高效的。
第二种:
db.users.find({"age":{"$gte":21,"$lte":30}})
这是一个多值查询,查找到多个值相匹配的文档,MongoDB会使用索引中的第一个键“age”得到匹配文档。如果使用“username”做查询,该索引不起作用。
第三种:
db.users.find({"age":{"$gte":21,"$lte":30}}).sort({"username":1})
这也是一个多值查询,与上一个类似,只是这次需要对查询结果进行排序。MongoDB需要在内存中对结果进行排序,不如上一个高效。
删除索引的命令如下:
db.users.dropIndex('age_1_username_1')
删除users集合中名字为'age_1_username_1'的索引。
所有数据库的索引信息都存储在system.indexes集合中,这是一个保留集合,不能在其中插入或者删除文档,只能通过ensureIndex和dropIndex对其进行操作。
使用如下命令可以获取users集合上的索引信息:
db.users.getIndexes()
结果如下:
[ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.users" }, { "v" : 1, "key" : { "username" : 1 }, "name" : "username_1", "ns" : "test.users" }, { "v" : 1, "key" : { "age" : 1, "username" : 1 }, "name" : "age_1_username_1", "ns" : "test.users" } ]
集合中的每一个索引都有一个名称,用于唯一标识这个索引,也可以用于服务器端来删除或者操作索引。索引的默认形式是keyname1_dir1_keyname2_dir2_..._keynameN_dirN,其中keynameX是索引的键,dirX是索引的方向(1或者-1)。如果索引中包含两个以上的键,这种命名方式就显得比较笨重了,但是我们可以通过ensureIndex指定索引的名称:
db.users.ensureIndex({"a":1, "b":1, ..., "z":1},...{"name":"test_name"})
另外需要注意索引的名称长度是有限制的,所以新建复杂索引时可能需要自定义索引名称。调用getLastError就可以知道索引是否创建成功,或者失败的原因。