千万级 MongoDB 数据索引优化实践

image.png

小李是这家公司的后端负责人,突然有一天下午,收到大量客服反馈用户无法使用我们的APP,很多操作与加载都是网络等待超时。

收到信息后,小李立马排查问题原因,不过多一会,定位到数据库出现大量慢查询导致服务器超负荷负载状态,CPU居高不下,那么为什么会出现这个情况呢,此时小李很慌,经过查询资料,开始往慢查询方向探究,果不其然,由于业务数据增长迅猛,对应的数据表没有相应查询的索引数据,此刻小李嘴角上扬,面露微笑,信心百倍上手的给数据库相关数据表加上了索引字段。但是情况并没有好转,线上依旧没有恢复,经验使然,最后只能采取降级的方案(关闭此表的相关查询业务)临时先恢复线上正常。

但是事情并没有结束,问题没有根本性的解决,公司和自己依旧非常在意这个问题的解决,晚上吃饭的时候,小李突然想起了自己有认识一个行业大佬(老白)。把问题跟老白说了一遍,老白并没过多久,很快就专业的告诉了小白哪些操作存在问题,怎么样可以正确的解决这个问题,加索引的时候首先要学会做查询分析,然后了解ESR最佳实践规则(下面会做说明),小李没有因为自己的不足感到失落,反而是因为自己的不足更是充满了求知欲。

数据库索引的应用有哪些优秀的姿势呢?

MongoDB 索引类型

单键索引

db.user.createIndex({createdAt: 1}) 

createdAt创建了单字段索引,可以快速检索createdAt字段的各种查询请求,比较常见
{createdAt: 1} 升序索引,也可以通过{createdAt: -1}来降序索引,对于单字段索引,
升序/降序效果是一样的。

组合索引

db.user.createIndex({age: 1, createdAt: 1}) 

可以对多个字段联合创建索引,先按第一个字段排序,第一个字段相同的文档按第二个字段排序,依次类推,所以在做查询的时候排序与索引的应用也是非常重要。

实际场景,使用最多的也是这类索引,在MongoDB中是满足所以能匹配符合索引前缀的查询,例如已经存在db.user.createIndex({age: 1, createdAt: 1})
我们就不需要单独为db.user.createIndex({age: 1}) 建立索引,因为单独使用age做查询条件的时候,也是可以命中db.user.createIndex({age: 1, createdAt: 1}) ,但是使用createdAt单独作为查询条件的时候是不能匹配db.user.createIndex({age: 1, createdAt: 1})

多值索引

当索引的字段为数组时,创建出的索引称为多key索引,多key索引会为数组的每个元素建立一条索引

// 用户的社交登录信息,
schema = {
    …
    snsPlatforms:[{
        platform:String, // 登录平台
        openId:String, // 登录唯一标识符
    }]
}
// 这也是一个列转行文档设计,后面会说
db.user.createIndex({snsPlatforms.openId:1}) 

TTL 索引

可以针对某个时间字段,指定文档的过期时间(用于仅在一段时间有效的数据存储,文档达到指定时间就会被删除,这样就可以完成自动删除数据)
这个删除操作是安全的,数据会选择在应用的低峰期执行,所以不会因为删除大量文件造成高额IO严重影响数据性能。

部分索引

3.2版本才支持该特性,给符合条件的数据文档建立索引,意在节约索引存储空间与写入成本

db.user.createIndex({sns.qq.openId:1})
/**
 * 给qq登录openid加索引,系统其实只有很少一部分用到qq登录 ,然后才会存在这个数据字段,这个时
 * 候就没有必要给所有文档加上这个索引,仅需要满足条件才加索引
 */
db.user.createIndex({sns.qq.openId:1} ,{partialFilterExpression:{$exists:1}})

稀疏索引

稀疏索引仅包含具有索引字段的文档条目,即使索引字段包含空值也是如此。
索引会跳过缺少索引字段的所有文档。

db.user.createIndex({sns.qq.openId:1} ,{sparse:true})

注:3.2版本开始,提供了部分索引,可以当做稀疏索引的超集,官方推荐优先使用部分索引而不是稀疏索引。

ESR索引规则

索引字段顺序: equal(精准匹配) > sort (排序条件)> range (范围查询)

精确(Equal)匹配的字段放最前面,排序(Sort)条件放中间,范围(Range)匹配的字段放最后面,也适用于ES,ER。

实际例子:获取成绩表中,高2班中数学分数大于120的学生,按照分数从大到小排序
不难看出,班级和学科(数学)可以是精准匹配,分数是一个范围查询,同时也是排序条件
那么按照ESR规则我们可以这样建立索引
{"班级":1,"学科":1,"分数":1}

我们怎么分析这个索引的命中与有效情况呢?

db.collection.explain()函数可以输出文档查找执行计划,可以帮助我们做更正确的选择。
分析函数返回的数据很多,但我们主要可以关注这个字段

executionStats 执行统计

{
    "queryPlanner": {
        "plannerVersion": 1,
        "namespace": "test.user",
        "indexFilterSet": false,
        "parsedQuery": {
            "age": {
                "$eq": 13
            }
        },
        "winningPlan": { ... },
        "rejectedPlans": []
    },
    "executionStats": {
        "executionSuccess": true,
        "nReturned": 100,
        "executionTimeMillis": 137,
        "totalKeysExamined": 48918,
        "totalDocsExamined": 48918,
        "allPlansExecution": []
    },
    "ok": 1,
}

nReturned 实际返回数据行数

executionTimeMillis 命令执行总时间,单位毫秒

totalKeysExamined 表示MongoDB 扫描了N个索引数据。 检查的键数与返回的文档数相匹配,这意味着mongod只需检查索引键即可返回结果。mongod不必扫描所有文档,只有N个匹配的文档被拉入内存。 这个查询结果是非常高效的。

totalDocsExamined 文档扫描数

这几个字段的值越小说明效率越好,最佳状态是
nReturned = totalKeysExamined = totalDocsExamined
如果相差很大,说明还有很大优化空间,当具体业务还要酌情分析。
查询优化器针对该query所返回的最优执行计划的详细内容(queryPlanne.winningPlan)

stage

COLLSCAN:全表扫描,这个情况是最糟糕的
IXSCAN:索引扫描
FETCH:根据索引去检索指定document
SHARD_MERGE:将各个分片返回数据进行merge
SORT:表明在内存中进行了排序
LIMIT:使用limit限制返回数
SKIP:使用skip进行跳过
IDHACK:针对_id进行查询
SHARDING_FILTER:通过mongos对分片数据进行查询
COUNT:利用db.coll.explain().count()之类进行count运算
COUNTSCAN: count不使用Index进行count时的stage返回
COUNT_SCAN: count使用了Index进行count时的stage返回
SUBPLA:未使用到索引的$or查询的stage返回
TEXT:使用全文索引进行查询时候的stage返回
PROJECTION:限定返回字段时候stage的返回

我们不希望看到的(出现以下情况,就要注意了,问题可能就出现了)

COLLSCAN(全表扫描)
SORT但是没有相关的索引
超大的SKIP
SUBPLA在使用$or的时候没有命中索引
COUNTSCAN 执行count没有命中索引

然后是我们看看一条普通查询实际执行顺序

db.user.find({age:13}).skip(100).limit(100).sort({createdAt:-1})
image

图中可以看出,首先是IXSCAN索引扫描,最后是SKIP跳过数据进行过滤。

在executionStats每一个项都有nReturned 与 executionTimeMillisEstimate,这样我们可以由内向外查看整个查询执行情况,在哪一步出现执行慢的问题。

关于列转行文档设计模式

首先数据库索引并不是越多越好,在MongoDB单文档索引上限,集合中索引不能超过64个,一些知名大厂推荐不超过10个。

而在一个主表中,由于冗余文档设计,就会存在非常多信息需要增加索引,我们还是以社交登录为例子

常规设计

schema = {
…
        qq:{
            openId:String
        },
        wxapp:{
            openId:String,
        },
        weibo:{
            openId:String,
        }
…
}

// 每次增加新的登录类型,需要修改文档schema和增加索引
db.user.createIndex({qq.openId:1}) 
db.user.createIndex({wxapp.openId:1}) 
db.user.createIndex({weibo.openId:1}) 

列转行设计

schema = {
…
 snsPlatforms:[{
    platform:String, // 登录平台
    openId:String, // 登录唯一标识符
 }]
}
// 此时无论是新增登录平台还是删除,都不需要变更索引设计,一个索引解决所有同类型问题
db.user.createIndex({snsPlatforms.openId:1,snsPlatforms.platform:1})

提问:为什么openId要放在plaform前面呢?

这个小故事讲述了小李在遇到自身知识不能解决的问题,然后事情的处理思路与过程。每个人都有自己能力所不及的地方,那么这种情况要优先解决问题,或者降低事故的影响范围。

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