6.MongoDB之索引

本文深入探讨了MongoDB为何选用B树作为索引结构,对比了B树与B+树的特性。通过实例展示了MongoDB索引的创建、使用和管理,强调了索引对查询性能的影响。同时,分析了不同字段区分度对索引性能的贡献,指出在创建索引时应考虑字段的区分度。最后,文章还涉及了MongoDB的索引限制、TTL索引和不同类型的索引,为数据库性能优化提供了实用指导。
摘要由CSDN通过智能技术生成

前言:Mongodb索引使用什么数据结构?

中文互联网讨论的比较多的一个问题就是:“为什么使用B树;Mysql的InnoDB使用B+树”,例如

关于B树和B+树不做过于详细的对比,这里只是列出主要区别:

B树:B树的树内存储数据,因此查询单条数据的时候,B树的查询效率不固定,最好的情况是O(1)。我们可以认为在做单一数据查询的时候,使用B树平均性能更好。但是,由于B树中各节点之间没有指针相邻,因此B树不适合做一些数据遍历操作。

B+树:B+树的数据只出现在叶子节点上,因此在查询单条数据的时候,查询速度非常稳定。因此,在做单一数据的查询上,其平均性能并不如B树。但是,B+树的叶子节点上有指针进行相连,因此在做数据遍历的时候,只需要对叶子节点进行遍历即可,这个特性使得B+树非常适合做范围查询。

实际上mongodb使用B树可能是有问题的。为此专门找了下出处:

首先是官方使用手册有如下这句话:https://docs.mongodb.com/manual/indexes/

另外有找到了WT引擎的手册:http://source.wiredtiger.com/3.2.1/tune_page_size_and_comp.html 

 结论:这里经过多方求证几乎可以确定用的是B+树而不是B树。

索引数量:一般情况下10个以内都还算ok。如果集群中表特别多的话每个表都10个索引也不推荐(在WT引擎下每个索引都是一个文件,太多影响性能)。

mongodb可以对暂时还不存在的字段创建索引。这没有任何问题,后续你插入含有这个字段的数据这个索引相当于就有数据了。这和你针对已存在某个字段创建索引,后续又不断的插入含有这个字段的新的数据是等效的,每次新插入含有这个字段的数据后这条数据就会纳入到索引中去。

一、B树与B+树

为什么 MongoDB 索引选择B树,而 Mysql 索引选择B+树?

【原创】为什么Mongodb索引用B树,而Mysql用B+树? - 孤独烟 - 博客园

MySQL的InnoDB索引原理详解 - 割肉机 - 博客园

另外重点参考下这篇对于mysql InnDB原理的介绍:

MySQL的InnoDB索引原理详解 - 割肉机 - 博客园

看完B树(B+树)的介绍后可能会有些懵。会有如下问题:

问题一:既然B树、B+树已经包括数据本身了,那还需要额外存一份数据吗?(我们知道mongdb索引和集合都有对应的文件)

答:这里不要懵。“节点的数据”谁说一定要是原始数据?这里宽泛的理解为“引用”最合适,即可能是原始数据,也可能是标定原始数据的引用。看下面这个例子就明白了。

        实际上InnoDB的数据文件本身就是主键索引文件了(聚簇索引)。InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据若对Name列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。与之对应的是MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。

问题二、MonoDB会把数据也存在索引里面吗?

对于这个问题可以专门测一把。方法很简单往自己部署的mongodb中插入一条数据然后在数据文件所在的路径下grep关键词。看数据会不会在索引文件中出现就好了。测试结果如下:

第一轮测试:验证非索引字段落哪些文件?

执行如下命令: db.coll.save({name:"abshuozhuo",age:12})  结果如下:

插入瞬间journal日志就有记录了;一分钟后集合文件也有记录了;索引文件始终不会有记录。

第二轮测试:验证索引字段落哪些文件?

执行如下命令:  db.coll.save({_id:"abcshuozhuo",name:"zhangsan"})  结果如下:

插入瞬间journal日志就有记录了;一分钟后集合文件也有记录了;同时索引文件也有记录。如下:

结论:MongDB索引文件是不包含数据本身的,这符合集合数据和索引分别有对应文件的认识。关于B树、B+树中提到的“节点数据”其实存的是原始数据的引用。 

零、mongodb索引的一些限制(包括其他的限制):

MongoDB:17-MongoDB-索引限制及其他限制规则_琦彦-CSDN博客   

MongoDB Limits and Thresholds — MongoDB Manual

0、从2.6版本开始,如果现有的索引字段的值超过索引键的限制,MongoDB将不会创建索引。

1、索引字段值的长度限制。索引字段的值不能超过1024字节,否则创建索引失败;

2、单个集合索引数的限制。单个集合的索引数不能超过64个;

3、索引字段(自身)长度限制。自4.2版本开始mongodb移除了索引字段(自身)长度的限制。在以前的版本,包含命名空间和点分隔符(即<database name>.<collection name>.$<index name>)的完全限定索引名的长度不能超过127字节。

4、索引字段数的限制。对于一个复合索引不能超过32个字段;

5、不能联合使用text索引和地理空间索引。

测试数据准备:

/*
    准备如下数据插入名为test的collection中
    数据示例: {"name":"zhangsan", "class":2, "duration": 988}
*/
var batchSize = 100000;       //100万条数据
var name = ["zhangsan", "lisi", "wangwu", "sunliu"]
var sex = ["man", "female"]
var docs = [];
for (var i = 0; i < batchSize; i++) {
    docs.push( {
        name : name[i % 4],    //字符串
        sexual : sex[i % 2],   //区分度较小的数据    
        class : i % 5,         //数值类型
        duration : i % 1000,   //区分度较大的数值类型
        order : i              //数值完全独立的字段
    } );
}

db.test.save(docs)

db.test.count()

1、创建索引

(1)createIndex()方法

MongoDB使用createIndex()方法来创建索引。

注意:在3.0.0版本之前创建索引的方法为db.collection.ensureIndex(),之后版本采用db.collection.createIndex()方法。不过ensureIndex()还能用,只不过其作为createIndex的别名。

(2)语法及实例

createIndex()的基本语法格式如下:

db.collection.createIndex(keys, options)

#更具体的例子如下:
(1)单字段索引
db.集合名.createIndex( {"字段名": 1 },{"name":'idx_字段名'})

1)索引命令规范这里建议是 idx_<构成索引的字段名>。当然name只是可选项系统会默认生成索引名的
2)字段名后面 1代表升序  -1代表降序。当然对于单字段索引没有任何区别。

(2)组合索引
db.集合名.createIndex({"字段名1":-1,"字段名2":1},{"name":'idx_字段名1_字段名2',background:true})

(3)后台创建索引
db.集合名.createIndex({"字段名":1},{"name":'idx_字段名',background:true})

(4)为内嵌字段添加索引
db.集合名.createIndex({"字段名.内嵌字段名":1},{"name":'idx_字段名_内嵌字段名'})

(5)TTL索引
db.集合名.createIndex( { "字段名": 1 },{ "name":'idx_字段名',expireAfterSeconds: 定义的时间,background:true} )


#我们这里上述数据创建几个单字段索引
db.test.createIndex({"name":1})
db.test.createIndex({"class":1})
db.test.createIndex({"duration":1})

createIndex()接受的可选参数列表如下:

ParameterTypeDescription
backgroundBoolean建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 "background" 可选参数。 "background" 默认值为false
uniqueBoolean建立的索引是否唯一。指定为true创建唯一索引。默认值为false.
namestring索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。
dropDupsBoolean3.0+版本已废弃。在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 false.
sparseBoolean对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false.
expireAfterSecondsinteger指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。
vindex version索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。
weightsdocument索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。
default_languagestring对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语
language_overridestring对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language.

2、索引的分类

1、单字段索引(Single Field)

db.test.createIndex({"duration":1})

 上述语句对duration字段创建了一个单字段索引,其能加速duration字段的查询请求。mongodb默认创建的_id索引也是这种类型。

{duration: 1} 代表升序索引,也可以通过{duration: -1}来指定降序索引,对于单字段索引,升序/降序效果是一样的。

2、复合索引(Compound Indexes)

db.test.createIndex({"duration":1, "name": 1})

 单字段索引的升级版,多个字段联合索引。其实就是先按照第一个字段排序,第一个字段相同的情况下按照第二个字段排序,以此类推。显然字段出现的顺序对最终顺序是敏感的。

 值得注意的是组合索引满足的查询场景比单字段索引更丰富。其不光能满足多个字段组合起来的查询,也能满足所有匹配索引前缀的查询。举个例子:

{ "item": 1, "location": 1, "stock": 1 }

#对于上述这个组合索引,如下均是其前缀
Item字段,
Item字段和location字段
Item字段和location字段和stock字段

除了查询的需求能影响索引的顺序,字段值的分布也是一个重要的考量因素。例如class字段的取值非常有限,duration字段的取值要丰富很多;其结果就是拥有相同duration值的文档数会很少,此时先按照duration查找再按照class查找会更高效。

针对复合索引,索引中key的排序顺序决定了索引是否支持排序操作:

假如:一个对象包含username和date两个属性,如果创建索引如下:

    db.events.createIndex( { "username" : 1, "date" : -1 } )

则查询支持

    db.events.find().sort( { username: -1, date: 1 } )和

    db.events.find().sort( { username: 1, date: -1 } ).

但是不支持如下查询:

    db.events.find().sort( { username: 1, date: 1 } ).

详细信息:Compound Indexes — MongoDB Manual

举个例子,有如下数据:

{"name":"smith","age":48,"user_id":0}
{"name":"smith","age":30,"user_id":1}
{"name":"john","age":36,"user_id":2}
{"name":"john","age":18,"user_id":3}
{"name":"joe","age":36,"user_id":4}
{"name":"john","age":7,"user_id":5}
{"name":"simon","age":27,"user_id":6}
{"name":"joe","age":27,"user_id":7}

如果按照{name:1,age:-1}创建索引,mongdb会按照如下方式组织数据:
{"name":"joe","age":36,"user_id":4}
{"name":"joe","age":27,"user_id":7}
{"name":"john","age":36,"user_id":2}
{"name":"john","age":18,"user_id":3}
{"name":"john","age":7,"user_id":5}
{"name":"simon","age":27,"user_id":6}
{"name":"smith","age":48,"user_id":0}
{"name":"smith","age":30,"user_id":1}

用户名严格的按照字符升序排列,同名的数据按照年龄降序排列。这种情况下能高效的支持如下两种排序查询
db.coll.find().sort({name:1,age:-1})
db.coll.find().sort({name:-1,age:1})
但是对如下同为升序的排序查询就不那么有效了:
db.coll.find().sort({name:1,age:1})

如果想使其高效就要建立{name:1,age:1}的索引了。

即:按照组织好的顺序正着搂数据、反着搂数据都是可以的。打破组织好的顺序就不行了。

参见  CSDN  五.0 部分。

3、多key索引(Multi-key Index)

当索引的字段为数组时创建出的索引称为多key索引,多key索引会为数据的每个元素建立一条索引。例如对于上述数据我们加入一个habbit字段,这个字段的内容是一个数组。此时如果需要查询相同兴趣、爱好的人就可以利用habbit字段的多key索引。

{"name":"zhangsan", "class":2, "duration": 988, habbit:["football","running"]}

db.test1.createIndex({habbit:1})

4、内嵌字段索引

当某个字段内部也是一个json串,我们又想为其下的某个子字段创建索引也是可以的。如下:

{"name":"zhangsan", "class":2, "duration": 988, habbit:["football","running"], family:{number:4,addr:"zhejiang"}}


db.test1.createIndex({"family.number":1})

5、TTL索引(Time To Live)

 TTL索引是一种延时自动删除记录的一种索引。其必须是单字段索引,不支持复合索引且用于索引的单字段需要是Date类型。不能在已有索引的字段上在创建一个TTL索引,如果你想把非TTL索引改为TTL索引只能删除重建索引。

  • 数据超时

TTL索引会按设置的超时时间,自动定期把符合条件的文档删除,这里的符合是指索引字段的时间值超时:
1. 该字段如果是一个数组,并且有多个有效的时间值,那么mongodb会按最早的时间值来计算是否超时。
2. 如果某文档的该字段值不是Date类型或者数组中无有效时间值,那么该文档不会过期。
3. 如果一个文档没有索引字段,那么它也不会过期。

  • 删除操作

mongodb后台启动了一个线程来定期查询并删除符合条件的文档。但是注意这个周期可能是60秒,即每60秒触发一次删除任务即使你设置的时间是15秒也要至少等1分钟后才查询不到过期数据。
当TTL线程在执行的时候,你可以通过db.currentOp() 查询执行状态。

  • 删除操作的执行时间

如果以后台的方式创建了TTL索引,系统可以边创建索引边执行删除操作。如果是前台创建方式,则需要在创建完成后开始执行删除操作。
后台线程会每个1分钟执行一次删除操作,因此TTL索引并不能保证文档在过期的时间点就能被删除,从文档过期到被删除可能会有一段间隔时间,这个间隔包含了线程执行的间隔和执行时删除文档的时间。

#插入带有Date类型数据的记录
db.test1.save({ "_id" : 1, "createDate" : ISODate("2017-03-10T03:23:01.169Z") })
#创建时间为30秒的TTL索引
db.test1.createIndex({createDate:1}, {expireAfterSeconds:30})
#一分钟后执行发现没有上面插入的数据了
db.test1.find()

注:使用前最好了解其原理。

MongoDB学习笔记:TTL 索引的原理、常见问题及解决方案 - 腾讯云开发者社区-腾讯云

6、唯一索引  

 保证索引对应的字段不会出现相同的值,比如_id索引就是唯一索引。也就是说我们是可以实现“设置某个普通字段为primary key”的效果的。

#语法很简单就是普通设置索引,后面在加一个{unique:true}即可
db.collection.createIndex( <key and index type specification>, { unique: true } )

(1)Unique Index on a Single Field
db.test1.createIndex({"name":1},{unique:true})

(2)Unique Compound Index
db.members.createIndex( { groupNumber: 1, lastname: 1, firstname: 1 }, { unique: true } )

注:经过测试确实是好用的。 另外如果对集合某个已经不是unique的字段建唯一索引的话是会报错的;这个很好理解,都已经是集合中已有的数据了createIndex是不可能删数据的。

(1)单字段的唯一索引。如上对name字段创建一个唯一索引。

(2)唯一复合索引。你也可以强制一个针对复合索引的唯一约束。如果你对复合索引使用唯一性约束,mongodb会强制索引键值们组合后的唯一性。对于上述case就相当于groupNumber、lastname、firstname都相等就会收到限制;某个或者部分字段值相等都是没问题的[确定]。

详细信息参见官网:Unique Indexes — MongoDB Manual 

如何清空唯一键重复的文档?—— 通过使用mongoexport/mongoimport

(1)在3.0以前可以通过指定dropDups参数来实现重复值的文档删除。如下:

db.test1.createIndex({"name":1},{unique:true, dropDups:true})

(2)在3.0+的版本这个功能被去掉了。不过也是有方法实现数据去重的。

大致来讲步骤如下:①使用mongoexport工具将数据导出为json格式存档;②将原集合清空 ③给原集合创建索引 ④使用mongoimport工具导入之前存档的Json文件数据。

向db_shuozhuo的coll集合中插入如下数据,注意其中id是有重复的。

db.coll.insert({name:"zhangsan", age:20, id:10001})
db.coll.insert({name:"lisi", age:21, id:10002})
db.coll.insert({name:"wangwu", age:22, id:10003})
db.coll.insert({name:"liliu",age:23,id:10002})
1)bin文件夹下运行可执行文件将数据导出为Json格式存档→发现确实导出了可执行文件
./mongoexport --host 11.xxx.6.xxx:27017 -u monxxuser -p qq@mongo -d db_shuozhuo -c coll -o filename.json

2)mongo shell下清空原集合
db.coll.remove({})

3)mongo shell下对原集合目标字段创建唯一索引
db.coll.createIndex({id:1},{unique:true})

4)bin文件夹下运行可执行文件导入之前存档的Json文件数据
./mongoimport --host 11.xxx.6.xxx:27017 -u mongouser -p qq@mongo  -d db_shuozhuo -c coll --upsert filename.json

注意:

(1)注意这里可能会出现类似如下的报错。"2021-08-01T16:31:29.651+0800    could not connect to server: connection() error occured during connection handshake: auth error: sasl conversation error: unable to authenticate using mechanism "SCRAM-SHA-1": (AuthenticationFailed) Authentication failed."   其含义是你所指定的db中没有命令行中指定的用户名,处理同 这里
 

(2)可以通过--help查看帮助文档

7、Hashed Index

针对属性的哈希值进行索引查询,当要使用Hashed index时,MongoDB能够自动的计算hash值,无需程序计算hash值。注:hash index仅支持等于查询,不支持范围查询 。

注:设置hashed分片(另一个是范围分片)的一个前提条件就是要有这个分片键的hash索引。“single field hashed index or a compound hashed index (New in 4.4)”。这里

#hash索引的创建也很简单
db.test1.createIndex({'name':'hashed'});
db.test1.dropIndex('name_hashed')

8、其他索引

除此之外还有很多其他索引。部分索引、稀疏索引

3、查看索引、重建索引与删除索引

1、查看索引:

1)getIndexes():查看集合的所有索引;
2)getIndexKeys():查看索引键;
3)totalIndexSize():查看集合索引的总大小(当前集合所有索引所占用的空间大小);
4)getIndexSpecs():查看集合各索引的详细信息。

(1)查看所有索引getIndexes()

db.test.getIndexes()
[
        {
                "v" : 2,                    #索引版本
                "key" : {                   #索引的字段及排序方向
                        "_id" : 1           #根据"_id"字段升序所以
                },
                "name" : "_id_",            #索引名      
                "ns" : "db_msg_track.test"  #集合名
        },
        {
                "v" : 2,
                "key" : {
                        "duration" : 1
                },
                "name" : "duration_1",
                "ns" : "db_msg_track.test"
        },

        {
                "v" : 2,
                "key" : {
                        "class" : 1
                },
                "name" : "class_1",
                "ns" : "db_msg_track.test"
        }
]

(2)查看索引键getIndexKeys()

db.test.getIndexKeys()
[
        {
                "_id" : 1
        },
        {
                "duration" : 1
        },
        {
                "name" : 1
        },
        {
                "class" : 1
        }
]

(3)查看集合索引的大小totalIndexSize()

mongos> db.test.totalIndexSize()
4300800

2、重建索引reIndex()

db.collection.reIndex() — MongoDB Manual

就效果来说删除原索引后创建一个新的索引。感觉意义不是很大。

3、关于索引的删除MongoDB提供了如下方法:

1)dropIndex()方法用于删除指定的索引
2)dropIndexes()方法用于删除全部的索引

db.test.dropIndex("class_1")



db.test.dropIndexes()

4、索引交叉(Index Intersection)

        通常Mongodb值使用一个索引来完成大多数查询。但是,$or查询的每个字句可能使用不同的索引;此外,Mongodb可以使用多个索引的交集。关于索引交叉有以下几点:

(1)实际上,查询优化器很少选择使用索引交集的计划。

(2)默认情况下,基于散列的索引交集处于禁用状态,在计划选择中不支持基于排序的索引交集。优化器以这种方式运行,以防止错误的计划选择。

(3)索引、查询语句设计时不应依赖于索引交集。相反,应该使用复合索引。

(4)查询优化器的未来改进可能允许系统更好地识别索引交叉点计划有益的情况。

(5)要确定MongoDB是否使用索引交集,请运行explain();如果其中包括 AND_SORTED 或者 AND_HASH 阶段,就是使用到索引交集了的。

总结:就目前情况来说知道有这回事就好,但实际设计的时候应该当他不存在。

5、索引性能评测

单字段索引
db.test.createIndex({sexual:1})
db.test.createIndex({name:1})
db.test.createIndex({class:1})
db.test.createIndex({duration:1})

一、看看字段区分度对索引促进的影响

1、sexual字段加索引和不加索引的查询对比

特点:总共就两个值区分度特别小,该字段仅有两个值。

无索引:耗时450ms

有索引:耗时621ms

创建索引前db.test.totalIndexSize():9838592

创建索引后db.test.totalIndexSize():14368768   该索引占用了4500000

结论:当数值区分度非常小的时候(例如此处可选的值仅有两种)加索引效果不明显,甚至有些得不偿失。

2、name字段加索引和不加索引的查询对比

特点:区分度也很一般,该字段仅有5个值。

无索引:耗时433ms

有索引:耗时330ms

创建索引前db.test.totalIndexSize():14368768

创建索引后db.test.totalIndexSize():18984960   该索引占用了4600000

结论:字段可选值区分度增大到5时加索引对搜索速度是有正向促进作用的。

3、duration字段加索引和不加索引的查询对比

特点:区分度较大有1000中取值。

无索引:耗时441ms

有索引:耗时4ms

创建索引前db.test.totalIndexSize():18984960

创建索引后db.test.totalIndexSize():25841664   该索引占用了6900000

结论:字段可选值区分度较大的时候有索引能显著提升查询效率。

4、order字段加索引和不加索引的查询对比

特点:区分度较大有1000中取值。

无索引:耗时411ms

有索引:耗时1ms

创建索引前db.test.totalIndexSize():25841664

创建索引后db.test.totalIndexSize():30371840   该索引占用了4500000

结论:字段可选值区分度较大的时候有索引能显著提升查询效率。

由1、2、3、4可知:字段取值的区分度越大的建索引对查询效率的提高越明显;且随着取值区分度对索引占用的空间没有明显影响。

也就是说还是要优先对区分度较大的数据建索引(组合索引应该也是如此区分度大的最好放前面)。

二、看看索引对模糊查询的影响:

1、name字段加索引和不加索引的对模糊匹配性能影响

特点:区分度也很一般,该字段仅有5个值。

无索引:耗时577ms

有索引:耗时1195ms

结论:建立索引对string字段的模糊匹配没有正向促进作用(如下图为无索引和有索引的效果对比)。

分析:加索引后扫描的文档数确实由100万变成25万了,但是扫描的索引条目数由0变成了100万。

      

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

焱齿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值