五、MongoDB的索引
五、MongoDB的索引
1、简介
它就像是一本书的目录,如果没有它,我们就需要对整个书籍进行查找来获取需要的结果,即所说的全盘扫描;
而有了目录(索引)之后就可以通过它帮我们定位到目标所在的位置,快速的获取我们想要的结果。
2、演示
第一步,向用户集合users中插入100W条数据
1 var insertUsers = function() { 2 var start = new Date().getTime(); 3 for (var i = 1; i <= 1000000; i++) { 4 db.users.insert({ 5 "userid": i, 6 "username": "wjg" + i, 7 "age": Math.floor(Math.random() * 100), //年龄为0~99的随机整数 8 "createdate": new Date() 9 }) 10 } 11 var end = new Date().getTime(); 12 print("插入100W条数据共耗时" + (end - start) / 1000 + "秒"); 13 }
LZ的渣渣I3和4G内存总共耗时了484.623秒,约8分多钟。任务管理器里边可以很清楚的看到当时CPU、内存和磁盘使用率都普遍的增高。
第二步:查询用户名为“wjg465413”的文档对象
1 db.users.find({username:"wjg465413"}).explain("allPlansExecution") 2 { 3 "queryPlanner" : { 4 "plannerVersion" : 1, 5 "namespace" : "test.users", 6 "indexFilterSet" : false, 7 "parsedQuery" : { 8 "username" : { 9 "$eq" : "wjg465413" 10 } 11 }, 12 "winningPlan" : { 13 "stage" : "COLLSCAN", 14 "filter" : { 15 "username" : { 16 "$eq" : "wjg465413" 17 } 18 }, 19 "direction" : "forward" 20 }, 21 "rejectedPlans" : [ ] 22 }, 23 "executionStats" : { 24 "executionSuccess" : true, 25 "nReturned" : 1, 26 "executionTimeMillis" : 865, 27 "totalKeysExamined" : 0, 28 "totalDocsExamined" : 1000000, 29 "executionStages" : { 30 "stage" : "COLLSCAN", 31 "filter" : { 32 "username" : { 33 "$eq" : "wjg465413" 34 } 35 }, 36 "nReturned" : 1, 37 "executionTimeMillisEstimate" : 770, 38 "works" : 1000002, 39 "advanced" : 1, 40 "needTime" : 1000000, 41 "needFetch" : 0, 42 "saveState" : 7813, 43 "restoreState" : 7813, 44 "isEOF" : 1, 45 "invalidates" : 0, 46 "direction" : "forward", 47 "docsExamined" : 1000000 48 }, 49 "allPlansExecution" : [ ] 50 }, 51 "serverInfo" : { 52 "host" : "Jack", 53 "port" : 27017, 54 "version" : "3.0.3", 55 "gitVersion" : "b40106b36eecd1b4407eb1ad1af6bc60593c6105" 56 }, 57 "ok" : 1 58 }
说明:这里的explain方法相当于查询计划,它会返回给你查询过程的详细信息。它的参数有三种模式:“queryPlanner”(查询计划[默认])、“executionStats”(执行状态)和“allPlansExecution”(所有执行计划),这里我们只关注它返回给我们的以下几个信息。
1 "executionTimeMillis" : 865 //执行的毫秒数 注:如果你是第一次执行,可能会花费更长的时间 2 3 "totalDocsExamined" : 1000000 //共检查的文档数
第三步:在用户名“username”字段上加上索引
1 db.users.createIndex({ "username" : 1 })
重新执行上次的查询操作
1 db.users.find({username:"wjg465413"}).explain("allPlansExecution") 2 { 3 "queryPlanner" : { 4 "plannerVersion" : 1, 5 "namespace" : "test.users", 6 "indexFilterSet" : false, 7 "parsedQuery" : { 8 "username" : { 9 "$eq" : "wjg465413" 10 } 11 }, 12 "winningPlan" : { 13 "stage" : "FETCH", 14 "inputStage" : { 15 "stage" : "IXSCAN", 16 "keyPattern" : { 17 "username" : 1 18 }, 19 "indexName" : "username_1", 20 "isMultiKey" : false, 21 "direction" : "forward", 22 "indexBounds" : { 23 "username" : [ 24 "[\"wjg465413\", \"wjg465413\"]" 25 ] 26 } 27 } 28 }, 29 "rejectedPlans" : [ ] 30 }, 31 "executionStats" : { 32 "executionSuccess" : true, 33 "nReturned" : 1, 34 "executionTimeMillis" : 53, 35 "totalKeysExamined" : 1, 36 "totalDocsExamined" : 1, 37 "executionStages" : { 38 "stage" : "FETCH", 39 "nReturned" : 1, 40 "executionTimeMillisEstimate" : 0, 41 "works" : 2, 42 "advanced" : 1, 43 "needTime" : 0, 44 "needFetch" : 0, 45 "saveState" : 0, 46 "restoreState" : 0, 47 "isEOF" : 1, 48 "invalidates" : 0, 49 "docsExamined" : 1, 50 "alreadyHasObj" : 0, 51 "inputStage" : { 52 "stage" : "IXSCAN", 53 "nReturned" : 1, 54 "executionTimeMillisEstimate" : 0, 55 "works" : 2, 56 "advanced" : 1, 57 "needTime" : 0, 58 "needFetch" : 0, 59 "saveState" : 0, 60 "restoreState" : 0, 61 "isEOF" : 1, 62 "invalidates" : 0, 63 "keyPattern" : { 64 "username" : 1 65 }, 66 "indexName" : "username_1", 67 "isMultiKey" : false, 68 "direction" : "forward", 69 "indexBounds" : { 70 "username" : [ 71 "[\"wjg465413\", \"wjg465413\"]" 72 ] 73 }, 74 "keysExamined" : 1, 75 "dupsTested" : 0, 76 "dupsDropped" : 0, 77 "seenInvalidated" : 0, 78 "matchTested" : 0 79 } 80 }, 81 "allPlansExecution" : [ ] 82 }, 83 "serverInfo" : { 84 "host" : "Jack", 85 "port" : 27017, 86 "version" : "3.0.3", 87 "gitVersion" : "b40106b36eecd1b4407eb1ad1af6bc60593c6105" 88 }, 89 "ok" : 1 90 }
可以看到两次的查询计划有很大的差别,我们还是着重看下那两个属性值。
1 "executionTimeMillis" : 53 //执行的毫秒数 2 3 "totalDocsExamined" : 1 //共检查的文档数
加过索引之后查询这个文档所耗费的时间仅仅为53毫秒,并且扫描一次直接定位,性能提升了16倍。可见合理使用索引的重要性!
注:“_id”字段是Mongo为我们默认添加的索引,而且是唯一索引,保证了数据的唯一性,不可以移除。另外,使用limit(1)限制查询结果的数量也可以提高查询速度
3、索引的类型
a)、单一索引:可以在数据集上任意一个字段上建立索引,包括普通的属性键、内嵌文档以及内嵌文档中的属性键。
db.users.createIndex({ "username" : 1 }) //普通属性键的索引 //假设class是一个内嵌的文档 db.users.createIndex({ "class" : 1 }) //内嵌文档的索引 db.users.createIndex({ "class.classname" : 1 }) //内嵌文档中的属性键索引
索引方向:1表示升序,-1表示降序
b)、复合索引:以多个属性键为基础而建立得索引
1 db.users.createIndex({ "username" : 1, "age" : -1, "userid" : 1 }) //在“username”、“age”和“userid”上建立复合索引
索引前缀:通过建立上边的复合索引之后,Mongo就相当于同时拥有了三个索引一样,分别是{"username" : 1},{"username" : 1, "age" : -1}和{"username" : 1, "age" : -1, "userid" : 1},但是像{"age" : -1},{"userid" : 1}或者{"age" : -1, "userid" : 1}这三个索引并不会起作用。所以它会使用包含了前缀(首个)的索引的作为复合索引
c)、多键索引:为数组中的多个值建立索引以实现高效查询。
注:Ⅰ、不允许在多个数组上建立复合索引
Ⅱ、不能指定片键作为多键索引
Ⅲ、哈希索引不能是多键
Ⅳ、多键索引不支持覆盖查询
d)、地理空间索引和查询:Mongo提供了两种曲面类型的索引:2dsphere索引和2d索引。查询类型包括:包含(inclusion),交叉(intersection)和接近(proximity)
e)、文本索引:用来支持查询包含了字符串或者字符串数组的文档
1 db.users.createIndex({"username" : "text"})
注:文本索引不支持排序并且一个复合文本索引不能再包含其他任何索引了
f)、哈希索引:它可以在使用了哈希片键进行分片的数据集上进行索引,支持相等查询,但是不支持范围查询
1 db.users.createIndex({"username" : "hashed"})
4、索引特性
a)、TTL(Time-To-Live)索引:是一种具有生命周期的索引,它允许为每一个文档设置一个超时时间
1 db.users.createIndex({ "createdate" : 1 },{ "expireAfterSecs" : 60*60*24 })
说明:在“createdate”字段上建立一个TTL索引,当这个自段存在并且是日期类型,当服务器时间比“createdate”字段的时间晚60*60*24秒,即24小时时,文档就会被删除
b)、唯一索引:确保集合的每一个文档的指定键都有唯一值
1 db.users.createIndex({"username" : 1}, {"unique" : true})
c)、稀疏索引:Mongo里边的null会被看做值,如果有一个可能存在也可能不存在的字段,我们可以使用稀疏索引
1 db.users.createIndex({"age" : 1},{"sparse" : true})
4、索引操作
a)、查看所有索引
1 db.users.getIndexes()
b)、移除索引
1 db.users.dropIndex({"createdate1" : 1 })
c)、移除所有索引
1 db.users.dropIndexes()
d)、重建索引
1 db.users.reIndex()
说明:该操作会先删除所有索引,包括“_id”,然后重新创建所有索引