空闲时间来一篇 MongoDB explain 手记

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 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

参考文档 :

# MongoDB-SQL优化

Explain Results --- MongoDB Manual

相关推荐
LCG元2 小时前
【面试问题】JIT 是什么?和 JVM 什么关系?
面试·职场和发展
向前看-2 小时前
验证码机制
前端·后端
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹3 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭3 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫4 小时前
泛型(2)
java
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石4 小时前
12/21java基础
java
李小白664 小时前
Spring MVC(上)
java·spring·mvc