代码改变世界

Mongodb索引

2012-11-30 12:55  jiejiep  阅读(2467)  评论(3编辑  收藏  举报

引言

         从今年年初开始接触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});该查询不会使用索引排序。