如何正确使用索引
前言
索引的创建是为了查询的更有效率,更加快速。但索引创建的不合理也会影响查询性能,以及过多的索引更是会影响增,删,改的数据操作;且MongoDB的index数据结构是Btree结构,而MongoDB索引选择B树可能是因为MongoDB 是文档型的数据库,是一种nosql,它使用BSON格式保存数据,归属于聚合型数据库。被设计用在数据模型简单,性能要求高的场合。之所以采用B树,是因为B树key和data域聚合在一起。因此并不需要类似于区间查询的操作。
索引分类:
在创建索引之前,我想你应该清楚MongoDB都有哪些索引,这些索引都有哪些特性以及用处,所以这里我就简单概略的说明一下;
分类:
- Single Field Index,单键索引;
- Compound Field Index, 组合索引;
- Multikey Index,多键索引;
- Partial Index(部分索引)
- Hash Index;哈希索引;
- Text Index, 全文索引;
- Geospatial Index,地理空间索引;
逐一概述
Single Field Index(单键索引)
_id
主键索引其实就是create collection时默认的单键索引且唯一,默认是ObjectID数据类型,数据结构由12位字符组成,如下所示;当然你也可以自定义主键的值,一般情况下建议使用默认MongoDB自动分配的值;
时间戳(4) | 机器码(3) | 进程ID(2) | 随机数(3) |
---|
如何获得ObjectID对象里的时间呢?
rs1:PRIMARY> ObjectId("5d649cf9ee04fd9315b9a7be").getTimestamp()
ISODate("2019-08-27T03:01:13Z")
如何查询能够使用主键id作为筛选条件,那么相信查询速度是很快的,执行计划里可以看到IDHACK
而不是IXSCAN
,所以在查询的时候如果能够使用上主键id进行筛选尽量使用主键值;主键_id因为也是递增性质,所以也可以提供(时间)范围的查询;
rs1:PRIMARY> db.t1.find({"_id" : ObjectId("5d649d29ee04fd9315b9a7bf")}).explain("executionStats")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "testdb1.t1",
"indexFilterSet" : false,
"parsedQuery" : {
"_id" : {
"$eq" : ObjectId("5d649d29ee04fd9315b9a7bf")
}
},
"winningPlan" : {
"stage" : "IDHACK"
},
......
而如果是在普通字段上创建索引,就需要考虑到字段的基数大小(也就是字段上的重复值),基数越接近集合的总数据量,那么这个字段创建索引后的效果也好,使用此索引的查询效率也就越高;
单键索引创建语句如下,注,字段的排序顺序是1(升序)还是-1(降序)不重要,例如userid:1 与 userid:-1 在单键索引的使用上没有区别,不必纠结。
> db.t1.createIndex({userid:1})
如何查看某个字段上的基数呢?
rs1:PRIMARY> db.t1.distinct("userid").length
2
//或者
rs1:PRIMARY> db.t1.aggregate([
... {$group:{"_id":"$userid"}},
... {$count:"count"}
... ])
{ "count" : 2 }
Compound Field Index(组合索引)
组合索引就是所建索引包含多个字段,与单键索引不同的是,除了字段之间有前后顺序之分,字段本身的排序顺序也是非常重要的,所以在创建时一定要结合查询条件进行创建,否则你的find语句可能调用不上索引。
db.t2.createIndex( { "a": 1, "b": -1, "c":1 } )
组合索引有一个索引前缀(Index prefix)的特性,例如上述组合索引,当查询条件包含如下几种情况的字段时可以调用上索引
a
a,b
a,b,c
但是如下方式是无法使用上述索引的,这是在生产使用时需要注意的。
b
c
b,c
还有一个sort排序调用索引的问题,因为如果可以使用上索引本身的排序那么就没必要将数据放入内存排序,如果数据量很大,那么索引排序才是最优选择。
例如索引:{ username: 1, date: -1 }
,当sort字段的顺序与索引顺序一致时方可调用上索引排序,例如如下两条语句都可以实现:
db.t3.find().sort( { username: 1, date: -1 } );
db.t3.find().sort( { username: -1, date: 1 } );
但是如下语句时没办法使用上索引排序,原因就是其sort顺序与索引顺序不一致
db.t3.find().sort( { username: 1, date: 1 } )
最后说一句,其实我认为MongoDB的Compound Indexes与MySQL,或者Oracle的组合索引的使用原理其实是差不多,都是为了能让查询语句能尽量通过索引完成查询(即实现索引覆盖),尽量减少回表,或者减少内存排序。
所以在生产环境,针对开发提供的查询语句,如果能用一个组合索引满足所有查询语句的话,就建议使用组合索引。
Multikey Index(多键索引)
即在一个数组字段上创建索引时,MongoDB会自动将其创建为多键索引,不需要显示指定某个关键字,MongoDB会为每一个数组元素创建索引键值,从而支持数组字段的高校查询,多键索引能够基于字符串,数字以及嵌套文档进行创建。
比如如下数据结构:
{
"_id" : ObjectId("5ea98c62c5ea6f4c20378724"),
"userid" : 1,
"calls" : [
{
"phone" : "345"
},
{
"phone" : "555"
}
]
},
{
"_id" : ObjectId("5ea98c62c5ea6f4c20378725"),
"userid" : 2,
"calls" : [
{
"phone" : "333"
},
{
"phone" : "555"
}
]
}
在数组字段calls上创建完索引后
db.t1.createIndex({calls:1})
根据数组查询时就会调用上上述索引
>db.t1.find({"calls":{"phone":"333"}}).explain()
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN", //表示调用了索引
"keyPattern" : {
"calls" : 1
},
"indexName" : "calls_1", //表示调用了素银calls_1
"isMultiKey" : true,
"multiKeyPaths" : {
"calls" : [
"calls"
]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"calls" : [
"[{ phone: \"333\" }, { phone: \"333\" }]"
其实多键索引使用的机会不太多,当然如果你的业务的数据结构需要这样查询,那我建议你可以考虑一下数组字段上的多键查询,当然,上述索引无法支持如下查询方案:
>db.t1.find({"calls.phone":"333"}).explain()
},
"winningPlan" : {
"stage" : "COLLSCAN", //执行计划会显示全表扫描,而不走索引
"filter" : {
"calls.phone" : {
"$eq" : "333"
}
我们可以通过另一种多键索引的创建方法来满足上述查询,如下:
db.t1.createIndex({"calls.phone":1})
另:两个都是数组的字段不可以创建组合索引哦,会报错的,组合索引有且只能有一个字段是数组类型。
Partial Index(部分索引)
概括:部分索引仅对满足指定过滤条件的document建立索引,从而占用更低的索引空间,降低索引创建和维护上的性能消耗
部分索引一般用在,表的数据量比较大,但是只有一少部分数据经常被查询,那么我们就可以通过部分索引功能在降低Index Btree的深度和复杂度,从而提高查询效率。
例如某个字段不是所有的行数据都包含,那么我们就可以创建如下索引:
db.collection.createIndex({address:1},{partialFilterExpression:{address:{$exists:true}}})
当然还有全文索引和地理索引,由于用处比较特殊,这里我们就先不讲了,后续如果有需求,我们单独开一章进行详述。
下面我们来看看日常运维过程中,经常用到的和索引相关的技巧。
我们知道获取集合的索引信息是getIndexes()方法,那查看库下所有集合的索引信息:
db.getCollectionNames().forEach(function(collection) {
indexes = db[collection].getIndexes();
print("Indexes for " + collection + ":");
printjson(indexes);});
查看集合中各个索引的调用情况,可以通过这个数据来判断我们创建的索引是否有效,如果调用次数很少,那么需要考虑是否索引无效或者其他原因:
>db.mycoll.aggregate([{$indexStats: {}}])
{
"name" : "idx_msg_edate", //索引名称
"key" : {
"messageId" : 1,
"edate" : 1
},
"host" : "whdrcsrv402.cn.nonprod:27087",
"accesses" : {
"ops" : 8714423, //索引被调用次数
"since" : ISODate("2020-04-28T13:55:03.692+08:00") //上次mongod启动日期
}
}
注:
mongod进程重启后,索引调用次数统计清空;
如果想让查询语句强制走某个索引,可以使用hint
db.users.find().hint( "age_1" ) //age_1是索引名称
强制全表扫描:db.users.find().hint( { $natural : 1 } )
使用索引的一些限制
- 默认createIndexs操作使用的内存为500M,可通过参数
maxIndexBuildMemoryUsageMegabytes
修改。 - 一个索引条目的总大小必须小于1024字节(bytes),否则在创建索引时会报错
- 一个集合最多创建64个索引
- 索引名字长度不超过128个字符
- 组合索引最多包含31个字段