关于mongodb:千万级-MongoDB-数据索引优化实践

45次阅读

共计 4110 个字符,预计需要花费 11 分钟才能阅读完成。

小李是这家公司的后端负责人,忽然有一天下午,收到大量客服反馈用户无奈应用咱们的 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})

图中能够看出,首先是 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 后面呢?

这个小故事讲述了小李在遇到本身常识不能解决的问题,而后事件的解决思路与过程。每个人都有本人能鞭长莫及的中央,那么这种状况要优先解决问题,或者升高事变的影响范畴。

正文完
 0