引言
从今年年初开始接触Mongodb,就一直被如何建立最合理的索引这个问题折磨着,没办法,应用中的筛选条件太复杂。而关于Mongodb索引方面的中文资料并不多,所以只能在google上找找资料,然后就匆忙的开始用了。成长很曲折,也充满了惊喜,结合最近读的《Mongodb实战》,这里把一些经验和大家分享一下。基础语法此处略过,可参见《Mongodb权威指南》。
索引结构
Mongodb中的索引,是按照B树结构来存储的。B树有2个特点:
第一,它能用于多种查询,包括精确匹配(等于)、范围条件($in, $lt,$lte,$gt,$gte)、排序、前缀匹配和仅用索引的查询;
第二,在添加和删除键的时候,B树仍然能保持平衡。
索引类型
1. 唯一性索引
要设置唯一性索引,只需要在添加索引时设置 unique 选项即可:
db.RRP_RPT_BAS.ensureIndex({ID:1},{unique:true});
唯一索引,顾名思义,就是在集合中,每条文档的ID都是唯一的。当我们建立上述索引之后,如果再次插入同样ID的文档,则Mongodb会报异常,只有当我们使用安全模式执行插入操作时,才能获取到该异常。当然,我们有很多时候,是先有数据,然后再根据查询需要来建的索引,这个时候就可以ID有重复的,如果这个时候再在ID字段上建立唯一索引,会执行失败,那怎么样才能让Mongodb顺利建立索引,而又删掉重复的数据呢?这个时候需要设置 dropDups 属性。
db.RRP_RPT_BAS.ensureIndex({ID:1},{unique:true,dropDups:true});
注意:对于重复出现ID相同的文档,在ID字段上建立唯一索引时,Mongodb无法确定会保留哪一条。
2. 稀疏索引
建立稀疏索引,有利于帮助优化索引结构,减小索引的大小。
索引默认都是密集型的,也就是说,在一个有索引的集合里,每个文档都会有对应的索引项,哪怕文档中没有被索引键也是如此。比如一个表示产品信息的集合Product. 该集合中有一个表示产品所属分类的键:catagory_ids 。假设你在该键上构建了一个索引。现在假设有些产品没有分配给任何分类,对于每个无分类的产品, catagory_ids 索引中仍会存在像这样的一个null项,以便于查询没有产品分类的产品信息。但是有两种情况使用密集型索引就不太方便。
情况一:我们在设计Product集合时,在某个字段上url建立了唯一索引。假设产品在尚未分配url时就加入系统了,如果url上有唯一索引,而你希望插入多个 url 为空的产品信息,那么第一次插入会成功,但是手续会失败,因为索引里已经存在一个 url 为 null 的项了。
情况二:比如有一个博客网站,支持匿名评论。文档结构如下:
{
ID:20121130102121222,
TITLE:"Mongodb索引优化",
AUTHOR:"JIEJIEP",
COMMENTS:[
{
USERID:NULL,
CONT:"文章一般",
CMT_TIME:"2012-11-30"
},
{
USERID:NULL,
CONT:"有点意思",
CMT_TIME:"2012-11-30"
},
{
USERID:"jimmy",
CONT:"神奇的Mongodb",
CMT_TIME:"2012-11-30"
}
]
}
集合中包含大量评论的用户ID为空的情况,如果我们在 COMMENTS.USERID 上建立一个索引,那么该索引中会存在大量的为null的索引项。这样就会增加索引的大小,在添加和删除COMMENTS.USERID 为null 的文档时也需要更新索引,这都会影响性能。而我们又很少会去查询 COMMENTS.USERID 为null 的情况,所以索引中保存为null的索引项意义不大。
对于以上两种情况,我们建议使用稀疏索引。只需要设置 sparse 选项。
db.RRP_RPT_BAS.ensureIndex({ID:1},{sparse:true});
3. 多键索引
就是我们经常说的复合索引,它包含多个键。我会结合实例重点讲一下这类索引。
索引作用规则
如果我们建立这样一个索引:db.coll.ensureIndex({a:1,b:1,c:1});
那实际上可以利用的索引有: {a:1},{a:1,b:1},{a:1,b:1,c:1}
比如:
db.coll.find({a:123});
db.coll.find({a:123,b:”xxxx”}) ,
这2个查询都会利用 {a:1,b:1,c:1} 索引。
那我们怎么知道哪个查询使用了什么索引呢,这就要借助 explain 工具了。
现在我们有一个集合 txt_nws_bas,50W条数据 。包含键 id ,tit , cont, pub_dt, typ_code, fld_nation, fld_object 。
我们建立一个这样一个索引:{ "TYP_CODE" : 1, "FLD_OBJ" : 1, "FLD_NATION" : 1 }
> db.txt_nws_bas.find({TYP_CODE:1101,FLD_OBJ:1101}).sort({FLD_NATION:-1}).explain();
{
"cursor" : "BtreeCursor TYP_CODE_1_FLD_OBJ_1_FLD_NATION_1 reverse", --BtreeCursor 表示查询使用了索引,否则为 BasicCursor
"isMultiKey" : true, --是否使用多键索引
"n" : 8, --返回条数
"nscannedObjects" : 8, --扫描文档条数
"nscanned" : 8, --扫描索引数目
"nscannedObjectsAllPlans" : 8,
"nscannedAllPlans" : 8,
"scanAndOrder" : false, --为true则表示对查询结果进行了重新排序,而没有使用索引排序。这个参数很重要
"indexOnly" : false, --是否为覆盖索引查询
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0, --查询耗时,单位为毫秒,这个参数值越小,表示查询速度越快
"indexBounds" : {
"TYP_CODE" : [
[
1101,
1101
]
],
"FLD_OBJ" : [
[
1101,
1101
]
],
"FLD_NATION" : [
[
{
"$maxElement" : 1
},
{
"$minElement" : 1
}
]
]
},
"server" : "bd130:27017"
}
接下来,我们来讨论一下,Mongodb查询优化器是如何来选择最合适的索引的。
首先我们来说明一下Mongodb查询优化器选择理想索引的原则:
1. 避免 scanAndOrder。如果查询中包含排序,尝试使用索引进行排序
2. 通过有效的索引约束来满足所有字段-尝试对查询选择器里的字段使用索引
3. 如果查询包含范围查找或者排序,那么对于选择的索引,其中最后用到的键需能满足该范围查找或者排序。
查询首次运行时,优化器会为每个可能有效适用于该查询的索引创建查询计划,随后并行运行这个计划,nscanned 值最低的计划胜出。优化器会停止那些长时间运行的计划,将胜出的计划保存下来,以便后续使用。
最后,我们来介绍一下索引应用规则。(只讲多键索引)
索引:{ "TYP_CODE" : 1, "FLD_OBJ" : 1, "FLD_NATION" : 1 }
1. 精确匹配
精确匹配第一个键、第一个和第二个键,或者第一、第二和第三个键。
db.txt_nws_bas.find({TYP_CODE:1634});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100,FLD_NATION:420});
注意:
db.txt_nws_bas.find({TYP_CODE:1634}).sort({FLD_NATION:1}); 无法使用索引排序,数据量大时,执行该操作会报错。
> db.txt_nws_bas.find({TYP_CODE:1101}).sort({FLD_NATION:-1}).explain();
Fri Nov 30 12:41:50 uncaught exception: error: {
"$err" : "too much data for sort() with no index. add an index or specify a smaller limit",
"code" : 10128
}
2. 范围匹配
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:{“$in”:[1100,1101,1102]}});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100}).sort({FLD_OBJ:1});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100,FLD_NATION:{“$in”:[420,9000]}});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100}).sort({FLD_NATION:1});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_NATION:420}).sort({FLD_NATION:1});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100,FLD_NATION:{“$in”:[420,9000]}}).sort({FLD_NATION:1});
注意:
db.txt_nws_bas.find({TYP_CODE:1101,FLD_OBJ:{"$in":[1101,1102,1100]}}).sort({FLD_NATION:-1});该查询不会使用索引排序。