mongoDB索引

mongo支持多种index类型,其最大的作用就是提高查询性能,可以避免对collection的逐文档扫描。index保存指定的或者多个field值,并且按照field值的顺序排序,索引条目的有序性使得“相等比较eq”、range查询等性能很高。
mongo内部的索引结构采用B-/+树来实现,单次对索引的查询最多访问h(层高)个节点,时间复杂度为O(logN)。

基本操作命令

//创建索引
db.collec.createIndex({age:1,name:1},{unique:true,sparse:true,background:true,expireAfterSeconds:10,name:"index_name"})

db.collection.getIndexes()
db.collection.dropIndex("indexName")
db.collection.dropIndexes() //删除所有
db.col.totalIndexSize() // 查看索引大小 (bytes)
db.col.totalSize() //集合大小 (bytes)

//unique sparse background expireAfterSeconds参数下面会有说明

注意在 3.0.0 版本前创建索引方法为 db.collection.ensureIndex(),之后的版本使用了 db.collection.createIndex() 方法,ensureIndex() 还能用,但只是 createIndex() 的别名。

  • mongo提供两种建所以的方式foregroundbackground。foreground(默认)即前台操作,它会阻塞用户对数据的读写操作直到index构建完毕,即任何需要获取read、write操作都会阻塞;background即后台模式,不阻塞数据读写操作,独立的后台线程异步构建索引,此时仍然允许对数据的读写操作;其中background比foreground更加耗时。

索引分类

下面我们要说的mongo最常用的几种索引类型,有_id索引 单字段索引 组合索引 复合索引 地理空间索引 文本索引 哈希索引

  1. _id索引:每个document都必须有_id字段,如果不指定,mongodb会默认生成;_id字段为内置的“唯一索引”,整个collection中不会存在_id值相同的document,这是mongodb的内部机制,开发者无法改变,也不需要开发者显式的为_id建立索引,当然也无法remove它。通过_id字段来查询document是性能极高的,所以如果尽可能的通过_id查询或者update数据。
  2. 单字段索引 也叫普通索引,对单个字段建立的索引
  3. 组合索引Compound 对多个field建立的索引。比如{user:1,age:-1},索引条目按user正序排列,相同user索引条目再根据age值倒序排列。需要特别注意的是索引的field顺序很重要!。需要结合具体的查询和排序去分析,下面会提到建组合索引的注意事项
  4. 复合键索引(Multikey) 对document中的数组字段创建索引,就是复合键索引
  5. 地理空间索引(Geospatial) 为了支持高效的地址位置数据查询,mongodb提供了2种特殊的索引,2d索引(坐标平面)和2sphere索引(经纬球面)
  6. 文本索引(Text) 支持对collection中的string内容进行搜索操作,即text search;mongodb内置支持几种语言的分词,不过这种文本搜索的能力与专业的搜索引擎相比,还相当简陋。不支持中文~
  7. 哈希索引(Hash) 主要使用场景为“hash sharding”,即在“Sharding”架构模式中,对sharding key使用hash索引,以便数据的sharding和路由操作。hash索引只支持一个字段,它不能和其他字段组合,同时也不能是“unique”,使用db.<collection>.createIndex({a : "hashed"})来创建hash索引。

索引属性(property)

  1. 唯一索引(Unique):即索引字段的值是唯一的,全局无重复;如果尝试存入重复的值(write),操作将会被拒绝。可以通过db..createIndex({ “userid” : 1}, {unique : true})创建唯一索引。我们可以对一个组合索引使用unique属性,表示它们的组合值不能重复,默认情况下unique为false。如果插入的document不包含某个索引字段,那么此字段的值保存为null,mongodb也会用unique约束这样的文档,即不包含某个字段的文档只能保存一条。比如唯一索引{“x” : 1, “y” : 1},只能插入一条{“x” : “content”}(等同于{“x” : “content”,”y” : null})。对于hash索引而言,则不能使用unique属性。unique通常与sparse一起使用。

  2. 稀疏索引(Sparse) :只有当document中包含索引字段时,才会为此文档建立索引,但是这些文档仍然能够保存成功·;因为mongodb的document结构是模式自由的,所以document中不包含某个Field也是“正常的”;稀疏索引将会忽略那些document中不包含索引字段的记录,因此稀疏索引并没有包含collection中的所有文档;非sparse索引,将会为那些缺失的字段存储为null,因此索引包含了全部文档。 我们可以将“Sparse”和“Unique”选项同时约束一个index,以避免包含重复值的document,同时还忽略那些不包含索引字段的document(不建立索引,但是文档仍然会被保存)。当sparse与unique同时约束索引时,比如createIndex({“score” : 1}, {sparse : true, unique: true}),如果文档中不包含score字段,那么文档都可以保存成功,但是不会对这些文档建立索引,但是对于包含了score字段的文档,那么score值则不能重复。

  3. TTL索引 对“date”、“timestamp”类型的字段建立TTL索引,并在索引选项中指定索引保存的时长,那么mongodb将会在一定时间之后自动移除那些“过期”的索引和documents。通过TTL索引,可以实现一个清洗数据的“定时任务”。TTL索引只能支持一个字段(同hash索引,不能对组合索引使用此特性),不能和其他字段组合,即一个索引中只能有一个字段;比如document中created字段类型为date,表示文档插入的时间,我们希望文档在3600秒后过期删除,那么可以通过db..createIndex({ “created” : 1},{expireAfterSeconds : 3600})创建索引,其中“expireAfterSeconds”为TTL索引的必须选项。如果document中不包含索引字段,文档将不会过期。mongodb将会创建一个后台线程,读取index值并删除那些过期的document;在createIndex时,mongodb也会在构建索引的同时检测过期,删除那些过期的数据。TTL索引和众多的缓存服务一样,不能保证数据过期则立即被删除(对用户操作不可见),mongodb扫描数据(从头到尾)是有时间间隔的(60秒),因此过期数据的删除可能存在延迟问题。对于“replica Set”,这个后台线程只在primary上运行,primary上的删除操作同步给secondaries。mongodb的存储引擎的特性决定,这种大量的随机删除会导致严重的存储碎片,索引如果collection中存在TTL索引时,将会开启“usePowerOf2Sizes”选项而且不能修改,“usePowerOf2Sizes”可以降低存储碎片的可能。collection.createIndex(new Document("created",1),new IndexOptions().expireAfter(15L,TimeUnit.MILLISECONDS));

索引的基本策略

虽然索引可以提高query的查询效率,当然也会增加write时带来的开支,增加内存、磁盘的使用。建索引时要根据业务的查询和排序条件来合理选择。

索引的选择性。是判定一个索引筛选数据的能力,越高效的索引其选择性越高,即可以排除掉更多的不匹配数据,更少的数据遍历。比如文档中有一个字段status,这个status有2个合法值“new”、“processed”,如果在status字段上建立索引,那么此索引的选择性就比较低,因为它只能筛选掉一半数据,这也意味着即使索引生效,也需要遍历整个collection中一半的数据才能得到最终结果,事实上对status建立索引是没有必要的。

  • 索引支持匹配最左前缀,比如索引{x: 1, y : 1}能支持 {x : 1} 和 {x : 1, y : 1}两个key的查询。
  • 尽量避免大量数据内存排序 设计索引时尽量能直接获取到排好序的结果,性能最好;如果不能,mongo可以将数据进行内存中的排序,在没有使用索引情况下排序操作所占用内存不得超过32M,否则将中断排序。
  • 多字段索引顺序很重要 sort操作使用索引时必须要和index顺序一致,比如索引{a : 1, b : 1},将不支持sort( { b : 1, a : 1});且排序的方向必须和index声明的保持一致,比如上述索引支持{a : 1, b : 1} 和 { a : -1, b : -1},但无法支持{a : 1, b : -1} 和 { a : -1, b : 1}。
  • 查询和排序字段结合的分析

     如果参与排序的字段(和顺序)和索引字段一样或者是索引的前缀字段,且查询条件中也包含索引的前缀字段,那么mongodb则可以使用索引进行排序,这是一种比较高效的做法。比如索引{ a : 1, b : 1, c : 1},如下查询均可以利用索引排序:
    db.data.find().sort( { a: 1 } )
    db.data.find().sort( { a: -1 } )
    db.data.find().sort( { a: 1, b: 1 } )
    db.data.find().sort( { a: -1, b: -1 } )
    db.data.find().sort( { a: 1, b: 1, c: 1 } )
    db.data.find( { a: { $gt: 4 } } ).sort( { a: 1, b: 1 } )
    如果参与排序的字段不是索引的前缀字段,具体是否能够利用索引排序,就要求查询条件的字段需要为索引的最左前缀字段,同时查询条件的字段和sort字段的也应该构成组合索引的左前缀,比如索引 { a : 1, b : 1, c: 1},那么此索引将支持如下几种排序方式:
    db.data.find( { a: 5 } ).sort( { b: 1, c: 1 } )
    db.data.find( { b: 3, a: 4 } ).sort( { c: 1 } )
    db.data.find( { a: 5, b: { $lt: 3} } ).sort( { b: 1 } )
    如果查询字段和sort字段不能构成组合索引的左前缀,那么将不能高效的利用索引排序,或者有可能不会使用此索引,比如:
    db.data.find( { a: { $gt: 2 } } ).sort( { c: 1 } )
    db.data.find( { c: 5 } ).sort( { c: 1 } )

优化建议

1.索引覆盖查询
mongoDB可以做索引覆盖查询,这种查询不会访问指针指向的文档,而是直接用索引的数据返回结果。只有当要查询返回的字段,都在索引中的时候。

2.常见慢查询:
1).不等于和不包含查询一般不使用索引 $ne 或者$nin
2).通配符在前面的模糊查询, like ‘%xxx’
3).无索引的count 查询 和 排序(复合索引顺序不匹配)
4).多个范围查询(范围列可以用到索引(必须是最左前缀),但是范围列后面的列无法用到索引)
5).skip跳过过多的行数
6).$where操作符

3.对现有的数据大表建立索引的时候,采用后台运行方式

db.test.ensureIndex({"username":1,"age":-1},{"background":true}) #默认情况下background是false。

查询计划explain

db.person.find( {age: 18} ).explain()可以查看mongo的查询计划,包括索引的使用、文档扫描的多少、查询步骤等信息

一个例子

mongo-9552:PRIMARY&gt; db.person.find({age: 18}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.person",
"indexFilterSet" : false,
"parsedQuery" : {
"age" : {
"$eq" : 18
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"age" : 1
},
"indexName" : "age_1",
"isMultiKey" : false,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"age" : [
"[18.0, 18.0]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "localhost",
"port" : 9552,
"version" : "3.2.3",
"gitVersion" : "b326ba837cf6f49d65c2f85e1b70f6f31ece7937"
},
"ok" : 1
}

stage常见类型

explain.queryPlanner.winningPlan.stageexplain.queryPlanner.winningPlan.inputStage等。
文档中仅有如下几类介绍

COLLSCAN 全表扫描

IXSCAN 索引扫描

FETCH 根据索引去检索指定document

SHARD_MERGE 将各个分片返回数据进行merge

但是根据源码中的信息,个人还总结了文档中没有的如下几类(常用如下,由于是通过源码查找,可能有所遗漏)

SORT 表明在内存中进行了排序(与老版本的scanAndOrder:true一致)

LIMIT 使用limit限制返回数

SKIP 使用skip进行跳过

IDHACK 针对_id进行查询

SHARDING_FILTER 通过mongos对分片数据进行查询

COUNT 利用db.coll.explain().count()之类进行count运算

COUNTSCAN count不使用用Index进行count时的stage返回

COUNT_SCAN count使用了Index进行count时的stage返回

SUBPLA 未使用到索引的$or查询的stage返回

TEXT 使用全文索引进行查询时候的stage返回

<完>