MongoDB第七讲索引基础

不论是关系型数据库还是NoQSL数据库,要获取足够高的查询效率,都需要通过索引来控制,我们首先通过一个电影库的实例来分析一下索引的基础概念。

索引的基础概念

电影库的文档结构如下所示

{
    "_id" : ObjectId("5a3672cecff3930a19f5703c"),
    "name" : "异形1",
    "type" : [ 
        "科幻", 
        "惊悚"
    ],
    "year" : 1989.0,
    "nation" : "美国",
    "score" : 4,
    "info" : {
        "director" : "斯科特",
        "stars" : [ 
            "西格妮·韦弗", 
            "汤姆·斯凯里特"
        ]
    },
    "reviews" : [ 
        {
            "author" : "jake",
            "content" : "good movie",
            "score" : 4.0
        }, 
        {
            "author" : "leon",
            "content" : "bad movie",
            "score" : 2.0
        }
    ]
}

第一个要明白的问题是:为什么要使用索引,假设电影库中存在了50000条电影信息,我如果希望找到《七宗罪》这个电影,而这个电影刚好在地30222多条,那意味着,我们需要一条条的查找,直到找到这部电影,等于需要查找3w多次,这样的效率显然是很低的,但是如果我们在电影

库中加入一个索引,有如下的信息

异形       0x11
阿甘正传    0x13
.....
七宗罪     0x23
低俗小说   0x43

第一个值是name第二个值是硬盘的位置,这样我们就可以快速的找到《七宗罪》这个电影,索引其实和我们的书的目录是一个道理,但是对于印刷书籍而言,主要的索引都只是基于章节内容来的(但现在的部分书籍,特别是计算机类的书籍,在最后都会加入关键字的索引)。对于数据库而言,我们可以增加很多的索引,对于电影库而言,我们除了需要根据名字来检索,还会涉及到根据类型来检索,所以我们可以为type来增加索引,一个type会对应多个硬盘地址,索引的样子如下所示

悬疑     0x12,0x22,0x33,0x03,...,0x42
惊悚     0x12,0x22,0x37,0x2D,...,0x45
爱情     0x13,0x56,0x77,0xa3,...,0x44
....

这样我们要根据电影的类型来查询就容易得多。已上索引根据电影的名称或者类型来查询都非常的容易,但实际应用中可能存在如下一种可能:我知道这个电影的类型,但是忘记的了电影的名字,但是如果让我看到名字我就可以想起这个电影,如果基于以上的索引,会存在一些问题。

首先如果根据name的索引,由于我们记不住名称,所以查询需要翻遍整个名称,显然是不合理的,由于记住了类型,可以使用基于type的索引,但是我们不得不找到索引的位置之后,还得一条条的读取列表的信息,效率虽然会比基于name的高一些,但依然有改进的空间。

我们可以对两个字段同时建立索引,type和name,先建立type之后建立name,此时就会得到如下的索引信息

悬疑
    七宗罪   0x23
    致命id   0x36
爱情
    阿甘正传  0x13
    怦然心动  0xA2
    ...

这样就可以快速的检索到我们想要的信息,这种索引称之为复合索引,需要注意的是这种索引的顺序一定要注意,如果先添加name之后添加type,就会得不到想要的结果,因为我们根据不清楚电影的名字,所以究竟该创建什么索引一定要根据查询需要来分析和设计,如果盲目的添加太多的索引,会增加内容的维护的成本,效率反而会降低,我们要确保驻留在内存的所有索引都是有效的才能提高查询效率。

最后需要大家了解另外一点,按照上述实例,由于已经创建了复合索引,而且是以type开始,那我们还有没有必要再为type创建一个单独索引呢?显然是不需要的,因为通过这个复合索引已经可以获取type的值的,但是有没有必要为name创建一个单独索引呢,这就是需要的,因为这个复合索引没有办法根据name来检索信息。

索引的建立和效率

索引分为单键索引和复合索引,单键索引只会为一个key创建索引,如果我们为电影的导演建立了索引,又为电影的评分建立了索引,此时我们需要检索某个导演的电影评分高于4分的电影。单键索引如下所示

索引名称info.director磁盘地址向下遍历索引名称 score磁盘地址
斯皮尔伯格0x1230x12
大卫芬奇0xA240x11
克里斯托弗诺兰0xB150xA5
大卫林奇0x2230xA0
….

当执行db.movies.find({info.director:"大卫林奇",scroe:{$gte:3}}) 查询时在具体查询的时候,查询优化器首先会根据info.director进行排序,之后根据score排序,然后取两个的交集。

如果使用导演名称和分数来建立复合索引,结构如下所示

索引名称(director-score)硬盘地址
斯皮尔伯格-30xAA
大卫芬奇-30xA2
大卫芬奇-40xB1
克里斯托弗诺兰-50xB2
….

此时查询优化器通过director很快就可以定位到导演名称,之后从这个位置开始检索分数,效率就高很多,但如果索引的顺序反过来是先建立score再建立info.director,效率就会低得多,如下所示

索引名称(director-score)硬盘地址
3-斯皮尔伯格0xAA
3-大卫芬奇0xA2
4-大卫芬奇0xB1
5-克里斯托弗诺兰0xB2
….

查询优化器首先会找到大于等于3分的所有数据,然后一条条去获取info.director中的数据,这种效率比单键索引还要低很多,所以再次证明,如果要使用复合索引,一定要确定好顺序,否则只会使你的查询效率变得更低。

MongoDB的索引类型

了解了索引的基本知识之后,我们需要了解MongoDB支持的几种索引类型:

1、唯一索引

唯一索引用来确保文档中的key的唯一性,如果为某个字段设置了唯一索引之后,添加了相同的信息,会抛出duplicate key的异常,创建索引的命令

db.user.createIndex({username:1},{unique:true})

2、稀疏索引

按道理来说,索引应该都是密集型的,特别对于关系数据库而言,由于有schema的限制,但是对于MongoDB而言,由于没有schema的限制,每个文档中可能有一些值是null的,有些key也是不存在的,此时如果为字段创建索引,会为所有的null值都创建索引,这样会增加索引的大小,一个比较特别的例子就是一些网站的留言,如果开启了匿名留言,此时有很多用户的id都是null,如果为用户的留言信息增加索引,将会存储大量的null值的多余索引。这种方式就需要创建稀疏索引

db.movies.createIndex({"reviews.author":1},{unique:false,sparse:true})

第二种情况是如果我们为某个key增加了唯一索引,但是这个key有可能存在null的情况,此时如果添加一个文档,第一个该key为null的可以添加,但是第二个为null的就违反了这个约束,就无法添加。诸如用户中如果有个字段foo是唯一的,但是有可能存在null的情况,此时如果希望添加唯一索引,必须设置该索引的sparse为true

db.user.createIndex({foo:1},{unique:true,sparse:true})

3、多键索引

MongoDB支持在一个数组上创建索引,此时会为每个数组中的元素都创建索引,只要检索其中任意一个元素会得到多个索引入口。

{
    "name" : "异形1",
    "type" : [ 
        "科幻", 
        "惊悚"
    ]
}
{
    "name" : "七宗罪",
    "type" : [ 
        "惊悚", 
        "犯罪", 
        "悬疑"
    ]
}

为type创建了索引之后,当检索”惊悚”这个type时会得到多个索引入口。

4、哈希索引

在MongoDB中默认是使用字符来进行排序的,MongoDB的索引存储结构是基于B-Tree的数据结构,这种结构类似于二叉查找树,但是却支持多个接点,这种存储方式如果整棵树偏向某一个子节点,会使得查询效率变低,如:假设我们一username做了唯一索引,但结果这些用户中基本都是s-z开头的人特别多,这就会使得这颗子树的节点偏多,查询效率会有所降低,此时我们就可以设置这个索引为哈希索引,哈希索引会将每个值利用哈希算法来重新编码,让整棵树平衡,这样可以提高查询的效率。

另外就是对于objectId而言,由于都是基于时间来生成的,看下面这些id

{
    "_id" : ObjectId("5a3672cecff3930a19f5703c")
}
{
    "_id" : ObjectId("5a3672cecff3930a19f5703d")
}
{
    "_id" : ObjectId("5a3672cecff3930a19f5703e")
}

这种id非常类似,在后面介绍的分布式时,这些数据会存储到一台机器上,这是非常有危害的,如果某个时刻有大量的插入请求,此时就意味着是一台机器来承受所有的压力,而哈希索引可以解决这种问题

db.users.createIndex({"_id":'hashed'})

5、地理空间索引

MongoDB支持基于位置的经纬度来建立索引,诸如在找位置相关的信息时有所帮助。

索引管理

MongoDB使用createIndex()方法创建索引,索引创建完成之后通过db.collection.getIndexes()可以查询该collection中存在的索引信息。

>db.user.createIndex({username:1},{unique:true})
>db.user.getIndexes()
[
    {
        "v" : 2,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "document.user"
    },
    {
        "v" : 2,
        "unique" : true,
        "key" : {
            "username" : 1.0
        },
        "name" : "username_1",
        "ns" : "document.user"
    }
]

我们发现有两个索引,一个是基于_id的,v表示版本信息,key表示对哪个字段添加索引,name是索引的名称,ns表示索引的名称空间,是基于document数据库中的user这个collection来创建索引。

使用dropIndex(indexName)可以删除一个索引

>db.user.dropIndex("username_1")
>db.user.getIndexes()
[
    {
        "v" : 2,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "document.user"
    }
]

下面我们将讨论一下,究竟该在什么时候构建索引,当然最理想的肯定是在创建表的时候创建索引,这样就会以增量的方式递增,但是事实往往不是这样理想,因为查询的问题都只会在数据量大的时候才会体现出问题,所以很多时候都需要在后期的项目运营过程之中来调整和优化索引,这所带来的问题就是,新构建索引会占用掉大量的时间,一般都建议在访问量较小的时候来处理这个操作,一般构建索引的时候会占用写锁,此时如果希望用户可以继续访问数据,可以选择后台构建索引。

db.test.createIndex({foo:1,bar:1},{background:true})

构建索引时会消耗大量的内存,对项目的运行的性能影响很大,此时我们可以考虑使用离线索引,离线索引一般用在分布式的环境中,通常可以将数据复制到一个接点,在那个节点上进行离线索引的构建,构建完成之后将此接点切换为主节点,继续在另外一台服务器上进行索引的构建。这些知识在后面的章节再来详细介绍。

另外就是如果进行了大量的修改,删除操作,难免会存在很多索引碎片,这些索引碎片没有用,但依然会占用内存,所以此时可以通过reIndex重建索引,重建索引时也是写锁定的。索引使用时也需要格外慎重。

db.test.reIndex()

索引的基本操作就是这么多,但是我们需要掌握的技术是,如何根据性能来设计和优化索引,下一部分将会详细介绍一套查询优化的方法。

    原文作者:孔浩
    原文地址: https://www.jianshu.com/p/a6a43bf6ded7
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞