🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
今天分享下mongodb中关于索引的基本操作,我们日常做开发都避免不了要对程序进行性能优化,而程序的操作无非就是CURD,通常我们又会花费50%的时间在R上面,因为Read操作对用户来说是非常敏感的,处理不好就会被人唾弃。 从算法上来说有5种经典的查找,具体的可以参见我的算法速成系列,这其中就包括我们今天所说的“索引查找”,如果大家对sqlserver比较了解的话,相信索引查找能给我们带来什么样的性能提升吧。 <!--more--> 我们首先插入10w数据,上程序说话: ``` db.person.drop();//删除person集合 //创建10W条数据 for(var i=0; i< 100000; i++){ db.user.insert({"name":"hxj"+i, "age" : i}); } > db.user.count(); 100000 ``` 已经有10W条数据了。 ## 性能分析函数(explain) 好了,数据已经插入成功,既然我们要做分析,肯定要有分析的工具,幸好mongodb中给我们提供了一个关键字叫做“explain",那么怎么用呢? 还是看程序,注意,这里的name字段没有建立任何索引,这里我就查询一个“name10000”的姓名。 ``` > db.user.find({"name" : "hxj"+10000}); { "_id" : ObjectId("55935a673cb05382291d31a0"), "name" : "hxj10000", "age" : 10000 } > db.user.find({"name" : "hxj"+10000}).explain(); { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.user", "indexFilterSet" : false, "parsedQuery" : { "name" : { "$eq" : "hxj10000" } }, "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "name" : { "$eq" : "hxj10000" } }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "YJC-PC", "port" : 27017, "version" : "3.0.4", "gitVersion" : "0481c958daeb2969800511e7475dc66986fa9ed5" }, "ok" : 1 } ``` 其中: ``` "stage" : "COLLSCAN", ``` `stage` 是`COLLSCAN`,说明没有走索引,走索引的话会显示`IXSCAN`。 基于mongo3.0,和2.x的版本有些地方会不大一样。 旧版的话,会有几个Key可以看下: `cursor`: 如果出现的是`BasicCursor`,就是说这里的查找采用的是“表扫描”,也就是顺序查找,很悲催啊。 `nscanned`:表示数据库浏览了多少个文档。 `n`: 最终返回了多少个文档。 `millis`:总共耗时多少毫秒。 ## 建立索引(ensureIndex) 在10w条这么简单的集合中查找一个文档要114毫秒有一点点让人不能接收(2.x版本),好,那么我们该如何优化呢?mongodb中给我们带来了索引查找,看看能不能让我们的查询一飞冲天..... ``` > db.user.ensureIndex({"name":1}); { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 } > db.user.find({"name" : "hxj"+10000}).explain(); { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.user", "indexFilterSet" : false, "parsedQuery" : { "name" : { "$eq" : "hxj10000" } }, "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "name" : 1 }, "indexName" : "name_1", "isMultiKey" : false, "direction" : "forward", "indexBounds" : { "name" : [ "[\"hxj10000\", \"hxj10000\"]" ] } } }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "YJC-PC", "port" : 27017, "version" : "3.0.4", "gitVersion" : "0481c958daeb2969800511e7475dc66986fa9ed5" }, "ok" : 1 } ``` 这里我们使用了ensureIndex在name上建立了索引。 `1`:表示按照name进行升序,`-1`:表示按照name进行降序。 这回`stage` 是`FETCH`。 如果是2.x版本,会有这些信息: `cursor`: 如果是`BtreeCursor`,这么牛X,mongodb采用B树的结构来存放索引,索引名为后面的“name_1"。 `nscanned`:表示数据库浏览了多少个文档。 `n`: 最终返回了多少个文档。 `millis`:总共耗时多少毫秒。 通过这个例子相信大家对索引也有了感官方面的认识了吧。 ## 唯一索引 和sqlserver一样都可以建立唯一索引,重复的键值自然就不能插入,在mongodb中的使用方法是: ``` db.user.ensureIndex({"name":1},{"unique":true}) /*删除所有数据,发现删除不了*/ > db.user.remove() 2015-07-01T11:29:38.579+0800 E QUERY Error: remove needs a query at Error (<anonymous>) at DBCollection._parseRemove (src/mongo/shell/collection.js:305:32) at DBCollection.remove (src/mongo/shell/collection.js:328:23) at (shell):1:9 at src/mongo/shell/collection.js:305 /*删除所有数据*/ > db.user.remove({}) WriteResult({ "nRemoved" : 100000 }) > db.user.ensureIndex({"name":1}, {"unique": true}); { "createdCollectionAutomatically" : false, "numIndexesBefore" : 2, "errmsg" : "exception: Index with name: name_1 already exists with different options", "code" : 85, "ok" : 0 } > db.user.count() 0 /*清空集合里数据*/ > db.user.drop() true > db.user.ensureIndex({"name":1}, {"unique": true}); { "createdCollectionAutomatically" : true, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 } > db.user.insert({"name":"yjc", "age": 22}); WriteResult({ "nInserted" : 1 }) > db.user.insert({"name":"yjc", "age": 23}); WriteResult({ "nInserted" : 0, "writeError" : { "code" : 11000, "errmsg" : "E11000 duplicate key error index: test.user.$name_1 dup key: { : \"yjc\" }" } }) ``` ## 组合索引 有时候我们的查询不是单条件的,可能是多条件,比如查找出生在‘1989-3-2’名字叫‘jack’的同学,那么我们可以建立“姓名”和"生日“的联合索引来加速查询。 ``` > db.user.insert({"name" : "hxc", "birthday" : "1989-2-2"}); WriteResult({ "nInserted" : 1 }) > db.user.insert({"name" : "jack", "birthday" : "1989-3-2"}); WriteResult({ "nInserted" : 1 }) > db.user.insert({"name" : "joe", "birthday" : "1989-2-22"}); WriteResult({ "nInserted" : 1 }) > db.user.insert({"name" : "mary", "birthday" : "1989-3-12"}); WriteResult({ "nInserted" : 1 }) > db.user.insert({"name" : "jr", "birthday" : "1989-3-2"}); WriteResult({ "nInserted" : 1 }) > db.user.ensureIndex({"name":1, "birthday":1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 2, "numIndexesAfter" : 3, "ok" : 1 } > db.user.ensureIndex({"birthday":1, "name":1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 3, "numIndexesAfter" : 4, "ok" : 1 } ``` 看到上面,大家或者也知道name跟birthday的不同,建立的索引也不同,升序和降序的顺序不同都会产生不同的索引,那么我们可以用getindexes来查看下person集合中到底生成了那些索引。 ``` > db.user.getIndexes() [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.user" }, { "v" : 1, "unique" : true, "key" : { "name" : 1 }, "name" : "name_1", "ns" : "test.user" }, { "v" : 1, "key" : { "name" : 1, "birthday" : 1 }, "name" : "name_1_birthday_1", "ns" : "test.user" }, { "v" : 1, "key" : { "birthday" : 1, "name" : 1 }, "name" : "birthday_1_name_1", "ns" : "test.user" } ] ``` 此时我们肯定很好奇,到底查询优化器会使用哪个查询作为操作: ``` > db.user.find({"name":"jack", "birthday":"1989-3-2"}).explain(); { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.user", "indexFilterSet" : false, "parsedQuery" : { "$and" : [ { "birthday" : { "$eq" : "1989-3-2" } }, { "name" : { "$eq" : "jack" } } ] }, "winningPlan" : { "stage" : "KEEP_MUTATIONS", "inputStage" : { "stage" : "FETCH", "filter" : { "birthday" : { "$eq" : "1989-3-2" } }, "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "name" : 1 }, "indexName" : "name_1", "isMultiKey" : false, "direction" : "forward", "indexBounds" : { "name" : [ "[\"jack\", \"jack\"]" ] } } } }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "YJC-PC", "port" : 27017, "version" : "3.0.4", "gitVersion" : "0481c958daeb2969800511e7475dc66986fa9ed5" }, "ok" : 1 } ``` 看到结果我们要相信查询优化器,它给我们做出的选择往往是最优的,因为我们做查询时,查询优化器会使用我们建立的这些索引来创建查询方案,如果某一个先执行完则其他查询方案被close掉,这种方案会被mongodb保存起来,当然如果非要用自己指定的查询方案,这也是可以的,在mongodb中给我们提供了hint方法让我们可以暴力执行。 ``` > db.user.find({"name":"jack", "birthday":"1989-3-2"}).hint({"birthday":1, "name":1}).explain(); { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.user", "indexFilterSet" : false, "parsedQuery" : { "$and" : [ { "birthday" : { "$eq" : "1989-3-2" } }, { "name" : { "$eq" : "jack" } } ] }, "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "birthday" : 1, "name" : 1 }, "indexName" : "birthday_1_name_1", "isMultiKey" : false, "direction" : "forward", "indexBounds" : { "birthday" : [ "[\"1989-3-2\", \"1989-3-2\"]" ], "name" : [ "[\"jack\", \"jack\"]" ] } } }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "YJC-PC", "port" : 27017, "version" : "3.0.4", "gitVersion" : "0481c958daeb2969800511e7475dc66986fa9ed5" }, "ok" : 1 } ``` ## 删除索引 可能随着业务需求的变化,原先建立的索引可能没有存在的必要了,可能有的人想说没必要就没必要呗,但是请记住,索引会降低CUD这三种操作的性能,因为这玩意需要实时维护,所以啥问题都要综合考虑一下,这里就把刚才建立的索引清空掉来演示一下:dropIndex的使用。 dropIndex()删除某个索引 dropIndexes()删除全部索引 先查看索引: ``` > db.user.getIndexes() [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.user" }, { "v" : 1, "key" : { "name" : 1, "birthday" : 1 }, "name" : "name_1_birthday_1", "ns" : "test.user" }, { "v" : 1, "key" : { "name" : 1 }, "name" : "name_1", "ns" : "test.user" }, { "v" : 1, "key" : { "name" : 1, "unique" : true }, "name" : "name_1_unique_true", "ns" : "test.user" } ] ``` 删除普通索引: ``` > db.user.dropIndex("name_1"); { "nIndexesWas" : 4, "ok" : 1 } ``` 删除全部索引: ``` > db.user.dropIndexes(); { "nIndexesWas" : 3, "msg" : "non-_id indexes dropped for collection", "ok" : 1 } ``` 查看还有什么索引 ``` > db.user.getIndexes() [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.user" } ] ```