MongoDB索引简单叙述

如何正确使用索引

前言

索引的创建是为了查询的更有效率,更加快速。但索引创建的不合理也会影响查询性能,以及过多的索引更是会影响增,删,改的数据操作;且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个字段
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容