小李是这家公司的后端负责人,忽然有一天下午,收到大量客服反馈用户没法使用咱们的APP,不少操做与加载都是网络等待超时。数据库
收到信息后,小李立马排查问题缘由,不过多一会,定位到数据库出现大量慢查询致使服务器超负荷负载状态,CPU居高不下,那么为何会出现这个状况呢,此时小李很慌,通过查询资料,开始往慢查询方向探究,果不其然,因为业务数据增加迅猛,对应的数据表没有相应查询的索引数据,此刻小李嘴角上扬,面露微笑,信心百倍上手的给数据库相关数据表加上了索引字段。可是状况并无好转,线上依旧没有恢复,经验使然,最后只能采起降级的方案(关闭此表的相关查询业务)临时先恢复线上正常。后端
可是事情并无结束,问题没有根本性的解决,公司和本身依旧很是在乎这个问题的解决,晚上吃饭的时候,小李忽然想起了本身有认识一个行业大佬(老白)。把问题跟老白说了一遍,老白并没过多久,很快就专业的告诉了小白哪些操做存在问题,怎么样能够正确的解决这个问题,加索引的时候首先要学会作查询分析,而后了解ESR最佳实践规则(下面会作说明),小李没有由于本身的不足感到失落,反而是由于本身的不足更是充满了求知欲。设计模式
数据库索引的应用有哪些优秀的姿式呢?数组
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索引会为数组的每一个元素创建一条索引app
// 用户的社交登陆信息, schema = { … snsPlatforms:[{ platform:String, // 登陆平台 openId:String, // 登陆惟一标识符 }] } // 这也是一个列转行文档设计,后面会说 db.user.createIndex({snsPlatforms.openId:1})
能够针对某个时间字段,指定文档的过时时间(用于仅在一段时间有效的数据存储,文档达到指定时间就会被删除,这样就能够完成自动删除数据)
这个删除操做是安全的,数据会选择在应用的低峰期执行,因此不会由于删除大量文件形成高额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版本开始,提供了部分索引,能够当作稀疏索引的超集,官方推荐优先使用部分索引而不是稀疏索引。
索引字段顺序: equal(精准匹配) > sort (排序条件)> range (范围查询)
精确(Equal)
匹配的字段放最前面,排序(Sort)
条件放中间,范围(Range)
匹配的字段放最后面,也适用于ES,ER。
实际例子:获取成绩表中,高2班中数学分数大于120的学生,按照分数从大到小排序
不难看出,班级和学科(数学)能够是精准匹配,分数是一个范围查询,同时也是排序条件
那么按照ESR规则咱们能够这样创建索引
{"班级":1,"学科":1,"分数":1}
db.collection.explain()
函数能够输出文档查找执行计划,能够帮助咱们作更正确的选择。
分析函数返回的数据不少,但咱们主要能够关注这个字段
{ "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前面呢?
这个小故事讲述了小李在遇到自身知识不能解决的问题,而后事情的处理思路与过程。每一个人都有本身能力所不及的地方,那么这种状况要优先解决问题,或者下降事故的影响范围。