首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164...
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca...
一. 前言
MySQL 有一套完整的性能优化方案,同样的,MongoDB 也有一套相关的方法论。
最近新起了一个项目,但是数据量起来后效果不是很好,配置了索引后实际效果还是很差,所以不得不深入学习了一下Explain.
基础准备 : MongoDB 索引创建
java
db.collection.createIndex({key: direction, options});
- `key` 指定索引的键,即要创建索引的字段或字段组合。
- `direction` 指定索引的排序顺序,1 表示升序,-1 表示降序。
- `options` 指定索引的其他选项,如是否在后台创建、是否设置过期时间等。
其中基于 Key 的组合可以区分单字段索引和多字段索引 :
db.collection.createIndex({name: 1, age: -1});
而 direction 又可以区分升序和降序索引 , 在 MongoDB 中索引是有序的。排序方式在查询/插入/删除时都会有影响。 在正序和倒序查询中,索引的排序尤为重要。
上述的案例中,就是name索引是升序,age索引为倒序。
最后创建索引的 options 又可以分为 :
- background : 通过后台创建,不影响现有应用的运行。
- unique : 唯一索引,即索引中的值唯一
- sparse :稀疏索引,不包含值得文档不会放入索引中。
- expireAfterSeconds : 索引有效期,超过有效期的索引会被删除。
上面温习了一些索引的创建方式,后面就来看看为什么我们建的索引没有生效,已经如何创建索引最有效。
二. Explain 分析方式
2.1 explain 语法及结果解析
MongoDB explain 用法很简单,在最后查询的时候加上 explain() 方法即可 :
java
db.getCollection("user").find().sort({ createTime: 1}).explain()
// 通常情况下会输出以下 Explain 报文 :
{
namespace: "testDB.user",
indexFilterSet: false, // 是否使用了索引过滤器
parsedQuery: { },
queryHash: "4E9F8D2A",
planCacheKey: "4E9F8D2A",
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
winningPlan: { // 对象包含查询的执行计划
stage: "SORT",
sortPattern: {
createTime: NumberInt("1")
},
memLimit: NumberInt("104857600"),
type: "simple",
inputStage: {
stage: "COLLSCAN",
direction: "forward"
}
},
rejectedPlans: [ ] // 被选择但是最终没有被执行的执行计划
}
东西很多 ,我们没必要都了解,主要从应用的视角来看待这些参数 :
- ❓ 索引过滤器代表什么 ?
通过 explain 计划里面的 indexFilterSet 可以判断是否使用了索引过滤器 ,该功能主要为了缩小索引扫描的范围 ,用一句通俗点的话来说 : 针对于某一类查询,它只能使用哪些索引
。 我们可以通过以下方式来配置 :
java
db.runCommand(
{
planCacheSetFilter: <collection>, //需要创建indexFilter集合
query: <query>, //指定哪类查询使用indexFilter
sort: <sort>, //排序条件
projection: <projection>, //查询字段
indexes: [ <index1>, <index2>, ...] //indexFilter可使用索引
}
)
- ❓ parsedQuery 里面一般是什么?
parsedQuery 包含查询的原始查询语句 , 即可以用来理解查询如何被 MongoDB 解析 ,同时可以避免一些语法错误 ,以下就是当我们使用查询语句的情况下 ,返回的效果 (PS :查询条件为空,此处即为空,例如无条件Sort)
- ❓一些其他的字段 :
- queryHash :当前查询的 Hash 值 ,用于唯一标识查询
- planCacheKey : 查询计划缓存Key ,用来匹配查询缓存,这里面涉及到查询条件等元素的匹配
- maxIndexedOrSolutionsReached : 针对 OR 条件查询时,达到了内部限制的索引组合数
- maxIndexedAndSolutionsReached : 针对 AND 条件。MongoDB 达到了索引组合数的限制。
- maxScansToExplodeReached :多索引条件查询时 ,无法将多个索引扫描合并为一个索引扫描
2.2 查询计划 winningPlan 里面的核心参数
当以上的参数了解完了,就可以开始了解最重要的查询计划了,查询计划分为 winningPlan 和 rejectedPlans ,
从 winningPlan
中看到最终被执行的执行计划
, 同时可以从 rejectedPlans
了解到有哪些执行计划是备选方案
。
而在这两个 plan 里面会有一些公共的字段 :
- stage : 在 MongoDB 一个查询中,会包括很多阶段,有的阶段是会使用索引的,有的阶段并不会,以下是主要的阶段
- COLLSCAN : 扫描整个集合,遍历出文档
- IXSCAN : 使用索引扫描集合
- FETCH : 从集合中检索文档
- SORT : 对文档解析排序
- LIMIT : 对返回结果进行限制时
- JOIN : 连接多个集合时
- GROUP : 对指端进行分组时
- inputStage : 查询的输入阶段,用于给后续阶段提供数据输入
**同时在 plan 中还有很多的字段用来描述当前阶段的特性功能 , 后面通过具体的案例来看 :
2.3 如何看懂 explain
❓ 如何判断是否使用了索引 ?
通常当 stage 为 COLLSCAN 时 ,就可以认定没有使用索引了,因为走的是全表。而非这个状态时,就需要具体分析了 :
java
// 以下是一个标准的索引使用返回 :
{
namespace: "testDB.user",
indexFilterSet: false,
parsedQuery: {
age: {
$eq: 30
}
},
//...........
winningPlan: {
stage: "FETCH", // 标识是在检索文档的阶段计划
inputStage: {
stage: "IXSCAN", // 使用索引进行集合扫描
keyPattern: { // 可以通过这字段判断使用了扫描索引
age: NumberInt("-1") // 键是索引中包含的字段名,值是该索引字段的数据类型
},
indexName: "idx_age", // 使用的索引名称
isMultiKey: false, // 标识索引键是否可以包含多个值 (这里不是指复合索引)
multiKeyPaths: {
// email: [ ], -- 如果是复合索引,这里就会显示多个
age: [ ]
},
isUnique: false, // 表示索引键是否必须是唯一的 (类似 MySQL 的唯一索引,不能有相同的文档)
isSparse: false, // 表示索引键是否可以为空
isPartial: false, // 表示索引键是否可以部分匹配
indexVersion: NumberInt("2"), // 标识索引的版本 , 用于跟踪索引的更改
direction: "forward", // 用来标识索引的方向 , 可以有升序和降序,和索引创建规则有关
indexBounds: { // 定义了索引扫描的范围
age: [
"[30.0, 30.0]" // 因为使用的查询条件是 {"age" : 30} , 所以范围上下限都能确定
]
}
}
},
rejectedPlans: [ ]
}
- isMultiKey 标识的是索引字段是否包含多个值 ,如果这个字段的类型是数组时 ,MongoDB 就会为每个数组值创建索引(
有待确定
)
❓ rejectedPlans 计划对比 :
通常如果两个条件都命中索引的时候,就会涉及到索引的拒绝策略了 :
db.getCollection("user").find({ $and : [{"age" : 30}, {"username" : "湖南21"}] }).explain();
- 可以看到 ,最终选择了走 username 的索引方式,而不是 age 的索引列表。
❓ 如果走的全表扫描,结果是什么样的?
java
{
namespace: "testDB.user",
indexFilterSet: false,
parsedQuery: {
type: {
$eq: "福建32"
}
},
winningPlan: { // 看不到索引的 keyPattern 和相关参数
stage: "COLLSCAN", // 可以看到,这里使用了全表
filter: { // 这里可以看到过滤条件
type: {
$eq: "福建32"
}
},
direction: "forward"
},
rejectedPlans: [ ]
}
2.4 explain 还有哪些参数 ?
db.getCollection("user").find({ "email": { $in: ["湖南21", "湖南22"] } }).explain(verbose)
explain 中实际上可以传入一个 verbose , 用来定义 explain 的范围 :
- 0: 只返回 queryPlanner 阶段的输出。
- 1: 返回 queryPlanner 和 executionStats 阶段的输出。
- 2: 返回 queryPlanner、executionStats 和 allPlansExecution 阶段的输出。
executionStats 阶段也是 explain 的大头 , 里面会包含查询执行的性能情况 :
java
{
executionSuccess: true, // 执行结果
nReturned: NumberInt("2469"), // 查询返回的文档数
executionTimeMillis: NumberInt("5"), // 执行的耗时
totalKeysExamined: NumberInt("2470"), // 查询扫描的索引键数
totalDocsExamined: NumberInt("2469"), // 查询扫描的文档数
executionStages: {
stage: "FETCH",
nReturned: NumberInt("2469"), // 查询返回的文档数
executionTimeMillisEstimate: NumberInt("0"), // 查询的估计执行时间
works: NumberInt("2470"), // 指定查询执行阶段执行的"工作单位"的数量
advanced: NumberInt("2469"),
needTime: NumberInt("0"),
needYield: NumberInt("0"), // 查询是否需要在执行过程中暂停以等待其他操作完成
saveState: NumberInt("2"), // 查询阶段暂停处理并保存其当前执行状态的次数(暂停的次数,锁等待释放)
restoreState: NumberInt("2"), // 恢复已保存的执行状态的次数 ,和上述类型
isEOF: NumberInt("1"), // 指定执行阶段是否已到达流的终点(limit 分页效果不同)
docsExamined: NumberInt("2469"), // 指定在查询执行阶段扫描的文件数量
alreadyHasObj: NumberInt("0"),
inputStage: {
// 省略重复字段。。。。。
keysExamined: NumberInt("2470"),
seeks: NumberInt("1"),
dupsTested: NumberInt("0"),
dupsDropped: NumberInt("0")
}
},
allPlansExecution: [ ]
}
- 什么叫工作单位 ?
- 可以理解为一个原子操作 ,例如检查一个索引键,从集合中获取一个文档等
- saveState 和 restoreState 的作用 ?
- 主观推测和锁定有关,此数越大,意味着资源竞争越多
总结
时间有限,这一篇没有应用的分析和对比 ,但是下一篇就准备安排上了。
这一篇不算很详细,只是一些基础的 ,想深入可以看看官方文档 Explain Results --- MongoDB Manual