在数据库开发中索引是非常重要的,对于检索速度,执行效率有很大的影响。本文主要描述了MongoDB中索引的使用,以及通过分析执行计划来提高数据库检索效率。
作为事例,在数据库中插入百万条数据,用于分析
>for(i=0;i<1000000;i++){ "i":i, "username":"user"+i, "age":Math.floor(Math.random()*120), "created":newDate() }在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":"BtreeCursorusername_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”时就表示当前查询没有使用索引,而建立索引后,该值为“BtreeCursorusername_1”,也可以看出来MongoDB使用的是B树来建立索引。
通过使用索引,数据库会对数据库中索引中所表示的字段保持已排序状态,也就是说,我们能够方便的针对该字段进行排序查询如:
>db.users.find().sort({"username":1}) ...MongoDB能够很快返回结果,但是这种帮助只能在查询字段在首位的情况下才能生效,如果该字段不在查询的首位,就可能无法使用到该索引带来的好处了,如:
>db.users.find().sort({"age":1,"username":1}) error:{ "$err":"toomuchdataforsort()withnoindex.addanindexorspecifyasmallerlimit", "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":"toomuchdataforsort()withnoindex.addanindexorspecifyasmallerlimit", "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":"BtreeCursorusername_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":"BtreeCursorusername_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”就表示,所有尝试进行的查询操作次数。
在MongoDB中,也可以建立唯一索引:
>db.users.ensureIndex({"username":1},{"unique":true})建立了唯一索引后,如果插入相同名称的数据,系统就会报错:
>db.users.insert({"username":"user1"}) E11000duplicatekeyerrorindex:test.users.$username_1dupkey:{:"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}通过执行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-26Sat14:12
Emacs24.3.1(Orgmode8.2.1)
Validate
本文内容总结:TableofContents,1基本索引,2联合索引,3索引类型,4索引管理,
原文链接:https://www.cnblogs.com/the-last-resort/p/3378715.html