mongo支持多种index类型,其最大的作用就是提高查询性能,可以避免对collection的逐文档扫描。index保存指定的或者多个field值,并且按照field值的顺序排序,索引条目的有序性使得“相等比较eq”、range查询等性能很高。
mongo内部的索引结构采用B-/+树来实现,单次对索引的查询最多访问h(层高)个节点,时间复杂度为O(logN)。
基本操作命令
//创建索引 |
注意在 3.0.0 版本前创建索引方法为 db.collection.ensureIndex(),之后的版本使用了 db.collection.createIndex() 方法,ensureIndex() 还能用,但只是 createIndex() 的别名。
- mongo提供两种建所以的方式
foreground
和background
。foreground(默认)即前台操作,它会阻塞用户对数据的读写操作直到index构建完毕,即任何需要获取read、write操作都会阻塞;background即后台模式,不阻塞数据读写操作,独立的后台线程异步构建索引,此时仍然允许对数据的读写操作;其中background比foreground更加耗时。
索引分类
下面我们要说的mongo最常用的几种索引类型,有_id索引
单字段索引
组合索引
复合索引
地理空间索引
文本索引
哈希索引
等
- _id索引:每个document都必须有_id字段,如果不指定,mongodb会默认生成;_id字段为内置的“唯一索引”,整个collection中不会存在_id值相同的document,这是mongodb的内部机制,开发者无法改变,也不需要开发者显式的为_id建立索引,当然也无法remove它。通过_id字段来查询document是性能极高的,所以如果尽可能的通过_id查询或者update数据。
- 单字段索引 也叫普通索引,对单个字段建立的索引
- 组合索引Compound 对多个field建立的索引。比如{user:1,age:-1},索引条目按user正序排列,相同user索引条目再根据age值倒序排列。需要特别注意的是
索引的field顺序很重要!
。需要结合具体的查询和排序去分析,下面会提到建组合索引的注意事项 - 复合键索引(Multikey) 对document中的数组字段创建索引,就是复合键索引
- 地理空间索引(Geospatial) 为了支持高效的地址位置数据查询,mongodb提供了2种特殊的索引,2d索引(坐标平面)和2sphere索引(经纬球面)
- 文本索引(Text) 支持对collection中的string内容进行搜索操作,即text search;mongodb内置支持几种语言的分词,不过这种文本搜索的能力与专业的搜索引擎相比,还相当简陋。不支持中文~
- 哈希索引(Hash) 主要使用场景为“hash sharding”,即在“Sharding”架构模式中,对sharding key使用hash索引,以便数据的sharding和路由操作。hash索引只支持一个字段,它不能和其他字段组合,同时也不能是“unique”,使用
db.<collection>.createIndex({a : "hashed"})
来创建hash索引。
索引属性(property)
唯一索引(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一起使用。 稀疏索引(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值则不能重复。
- 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> db.person.find({age: 18}).explain() |
stage常见类型
如explain.queryPlanner.winningPlan.stage
和explain.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返回
<完>