Java-150 深入浅出 MongoDB 索引详解:索引管理、explain 执行计划与性能瓶颈分析 - 详解

点一下关注吧!!!非常感谢!!持续更新!!!

AI篇持续更新中!(长期更新)

AI炼丹日志-31- 千呼万唤始出来 GPT-5 发布!“快的模型 + 深度思考模型 + 实时路由”,持续打造实用AI工具指南!

Java篇正式开启!(300篇)

目前2025年10月13日更新到:
Java-147 深入浅出 MongoDB 分页查询详解:skip() + limit() + sort() 实现高效分页、性能优化与 WriteConcern 写入机制全解析
MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务正在更新!深入浅出助你打牢基础!

大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!
大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解

请添加图片描述

索引分析

索引管理

创建索引并在后台运行(Background Index Creation):

db.COLLECTION_NAME.createIndex(
{"字段": 1或-1},  // 1表示升序索引,-1表示降序索引
{
background: true,  // 在后台异步构建索引
name: "自定义索引名称",  // 可选参数,指定索引名称
sparse: false,  // 可选参数,是否创建稀疏索引
unique: false  // 可选参数,是否创建唯一索引
}
);

使用说明:

  1. 后台索引构建不会阻塞数据库的其他操作,适合在生产环境使用
  2. 相比前台索引创建,后台索引构建速度较慢但影响较小
  3. 可以在已有数据的集合上创建索引
  4. 可以同时指定多个字段创建复合索引

应用场景示例:

  • 在用户集合上为email字段创建后台唯一索引:
db.users.createIndex({email: 1}, {background: true, unique: true})
  • 在产品集合上创建多字段后台索引:
db.products.createIndex(
{category: 1, price: -1, stock: 1},
{background: true, name: "product_query_idx"}
)

注意事项:

  1. 后台索引构建期间,查询可能无法使用新索引
  2. 可以使用getIndexes()查看索引创建进度
  3. 大量数据时索引构建可能需要较长时间
  4. 可能需要额外的磁盘空间临时存储索引数据

获取针对某个集合的索引

在MongoDB中,可以通过getIndexes()方法查看指定集合的所有索引信息。该命令会返回一个包含索引详细信息的数组,包括索引名称、键字段、索引类型等。

命令语法
db.COLLECTION_NAME.getIndexes()
db.wzk.getIndexes();

在这里插入图片描述

索引的大小

db.COLLECTION_NAME.totalIndexSize()
db.wzk.totalIndexSize();

在这里插入图片描述

索引的删除

注意: _id 对应的索引是删除不了的

db.COLLECTION_NAME.dropIndex("INDEX-NAME")
db.COLLECTION_NAME.dropIndexes()

explain 分析

MongoDB 的 explain() 方法是一个强大的查询分析工具,它接受不同的参数来控制返回信息的详细程度。通过设置不同的参数,我们可以查看不同层次的查询执行计划细节:

  1. queryPlanner 模式 (默认参数)

    • 这是最基本的执行计划分析模式
    • 返回的信息包括:
      • 查询优化器选择的获胜计划(winningPlan)
      • 被拒绝的候选计划(rejectedPlans)
      • 索引使用情况
      • 查询阶段(如 COLLSCAN 或 IXSCAN)
    • 示例:db.collection.find({name: "John"}).explain("queryPlanner")
  2. executionStats 模式

    • 提供实际的执行统计信息
    • 包含的信息有:
      • 执行时间(executionTimeMillis)
      • 扫描文档数量(nReturned/totalDocsExamined)
      • 索引使用效率(keysExamined)
      • 阶段执行时间(executionStages)
    • 在某些 MongoDB 版本中,这个参数与 allPlanExecution 功能相同
    • 示例:db.collection.find({age: {$gt: 30}}).explain("executionStats")
  3. allPlansExecution 模式

    • 最详细的执行计划分析模式
    • 返回所有候选计划的执行统计信息
    • 包含的内容与 executionStats 基本相同,但额外提供:
      • 所有候选计划的执行统计(allPlansExecution)
      • 查询优化器的决策过程
    • 常用于复杂的查询优化场景
    • 示例:db.collection.find({status: "active", score: {$gt: 80}}).explain("allPlansExecution")

这些参数的选择取决于具体的分析需求:

  • 对于简单的查询优化,queryPlanner 通常足够
  • 当需要了解查询实际执行性能时,应使用 executionStats
  • 对于复杂查询或需要比较不同执行计划时,allPlansExecution 最为合适

注意:不同 MongoDB 版本可能在参数行为和返回字段上有细微差异,使用时需参考对应版本的官方文档。

在这里插入图片描述
在这里插入图片描述

executionStats

在这里插入图片描述

返回逐层分析

在 MongoDB 的 explain() 执行计划分析中,executionTimeMillis 是最直观的性能指标之一,它反映了查询语句的实际执行时间。这个数值当然是越小越好,表明查询效率越高。具体来说,explain() 返回结果中会包含三个不同层级的 executionTimeMillis 值:

  1. executionStats.executionTimeMillis
    这是整个查询语句的完整执行时间,从查询开始到返回结果的总耗时。它包括了查询准备、索引扫描、数据检索等所有环节的时间总和。例如,一个查询如果显示该值为 50ms,表示从发送查询到获取结果共花费了 50 毫秒。

  2. executionStats.executionStages.executionTimeMillisEstimate
    这个值表示查询在检索 documents 并获取数据阶段所花费的时间。它反映了 MongoDB 从存储引擎中读取符合条件数据的时间开销。比如在这个阶段花费了 30ms,说明数据检索环节占用了较大部分查询时间。

  3. executionStats.executionStages.inputStage.executionTimeMillisEstimate
    这个指标表示查询使用索引(如果存在)扫描所花费的时间。它反映了 MongoDB 在索引结构中定位符合条件数据的时间。例如,如果该值为 10ms,说明索引扫描环节较为高效。

在实际性能调优中,这三个时间值的关系通常会呈现:

  • 当索引扫描时间(inputStage)占比较大时,可能需要优化索引结构
  • 当数据检索时间(executionStages)过长时,可能需要考虑数据分片或优化查询条件
  • 整体时间(executionTimeMillis)是评估查询性能的最终标准

典型的应用场景:

  1. 发现查询缓慢时,首先检查 executionTimeMillis 确认整体耗时
  2. 然后对比三个时间值,找出性能瓶颈所在环节
  3. 根据具体的时间分布情况采取相应的优化措施

在 MongoDB 查询性能分析的第二层,index(索引)与 document(文档)的扫描数量是影响查询效率的关键指标。这里我们重点讨论三个核心返回参数:

  1. nReturned:表示查询实际返回的文档数量,即结果集大小。例如,当执行 db.users.find({age: 25}).limit(10) 时,nReturned 最大值为10。

  2. totalKeysExamined:表示查询过程中扫描的索引条目数量。假设在 age 字段上有索引:

    • 理想情况下,如果索引能精确定位到目标文档,如 {age: 25} 的查询,可能只需要扫描少量索引键
    • 糟糕情况下,如范围查询 {age: {$gt: 20}} 可能需要扫描大量索引键
  3. totalDocsExamined:表示查询过程中扫描的完整文档数量。当出现以下情况时该值会较高:

    • 查询无法使用索引(全表扫描)
    • 虽然使用索引但需要回表获取完整文档(如查询包含非索引字段)
    • 使用低效的索引(如区分度低的索引)

这些指标直接影响 executionTimeMillis(查询执行时间),扫描的文档和索引条目越少,查询速度就越快。以一个电商商品查询为例:

// 商品表有100万文档,category和price字段有复合索引
db.products.find({
category: "electronics",
price: {$gt: 500}
})

理想查询状态应满足:

nReturned = totalKeysExamined = totalDocsExamined = 实际匹配的商品数量

这意味着:

  • 索引被完美利用,直接定位到目标文档
  • 没有不必要的索引或文档扫描
  • 查询只需读取必要的索引和文档数据

实际优化时,可以通过 explain("executionStats") 查看这些指标,并针对性地:

  1. 创建合适的索引
  2. 优化查询条件
  3. 使用覆盖索引
  4. 调整索引顺序
    来尽可能接近这个理想状态。

第三层,stage状态分析,详细解析MongoDB查询执行计划中的各种stage类型及其应用场景:

  1. COLLSCAN(全表扫描):

    • 最基础的查询方式,会扫描集合中的每个文档
    • 性能最低,当查询条件没有可用索引时会触发
    • 示例:对没有索引的字段进行查询:db.users.find({age: 25})
  2. IXSCAN(索引扫描):

    • 使用索引来定位文档
    • 性能较好,是优化的目标
    • 示例:对已建立索引的字段查询:db.users.find({username: "john"}).explain()
  3. FETCH(文档获取):

    • 根据索引指针获取完整文档
    • 通常出现在IXSCAN之后
    • 示例:使用索引查询后获取完整文档的阶段
  4. SHARD_MERGE(分片合并):

    • 在分片集群中合并来自不同分片的结果
    • 只出现在分片集群环境中
    • 示例:跨分片查询:db.users.find().explain()
  5. SORT(内存排序):

    • 在内存中对结果进行排序
    • 可能消耗大量内存
    • 示例:db.users.find().sort({age: 1}).explain()
  6. LIMIT(结果限制):

    • 限制返回结果数量
    • 示例:db.users.find().limit(10).explain()
  7. SKIP(结果跳过):

    • 跳过指定数量的文档
    • 示例:db.users.find().skip(5).explain()
  8. IDHACK(ID直接查询):

    • 直接使用_id字段进行查询的优化路径
    • 性能最好
    • 示例:db.users.find({_id: ObjectId("...")}).explain()
  9. SHARDING_FILTER(分片过滤):

    • 过滤不符合分片键条件的文档
    • 防止查询不必要分片
    • 示例:按分片键查询时出现
  10. COUNT(计数运算):

    • 执行计数操作
    • 示例:db.users.find().count().explain()
  11. TEXT(全文检索):

    • 使用全文索引进行文本搜索
    • 需要预先创建文本索引
    • 示例:db.articles.find({$text: {$search: "mongodb"}}).explain()
  12. PROJECTION(字段投影):

    • 限定返回字段
    • 可以减少网络传输和提高性能
    • 示例:db.users.find({}, {name: 1, age: 1}).explain()

在实际查询优化中,这些stage会以树形结构组合出现,例如一个典型的优化查询可能呈现为:IXSCAN → FETCH → PROJECTION → LIMIT。通过分析这些stage的组合和顺序,可以深入了解查询的性能特征和优化空间。

希望看到的Stage

在 MongoDB 查询优化中,合理的执行计划(execution plan)应该尽量使用索引扫描(IXSCAN)来提升查询性能。以下是针对普通查询期望看到的理想 stage 组合及其具体应用场景的详细说明:

  1. Fetch + IDHACK

    • 这是针对 _id 字段查询的最高效执行计划,直接通过主键(_id)定位文档。
    • 示例:查询 db.collection.find({_id: ObjectId("507f1f77bcf86cd799439011")}) 时,MongoDB 会直接使用 IDHACK 快速定位文档,再通过 Fetch 阶段获取完整文档内容。
  2. Fetch + IXSCAN

    • 通过普通索引快速定位文档位置,再获取完整文档数据。
    • 应用场景:对已创建索引的字段进行等值或范围查询(如 db.collection.find({age: {$gt: 25}})),IXSCAN 会先扫描索引条目,Fetch 阶段根据索引指针拉取完整文档。
    • 关键点:确保查询字段已正确创建索引(如 db.collection.createIndex({age: 1}))。
  3. Limit + (Fetch + IXSCAN)

    • 在索引扫描基础上结合 Limit 阶段,限制返回结果数量以提升性能。
    • 典型场景:分页查询(如 db.collection.find({status: "active"}).limit(10))。
    • 优化效果:Limit 会尽早截断数据流,避免不必要的 Fetch 操作。
  4. PROJECTION + IXSCAN

    • 通过覆盖查询(Covered Query)直接从索引中返回数据,无需访问文档。
    • 条件:查询的所有字段都包含在索引中,且未使用 _id 字段(或显式排除 {_id: 0})。
    • 示例:若索引为 {name: 1, age: 1},查询 db.collection.find({name: "Alice"}, {age: 1, _id: 0}) 会仅通过 IXSCAN 完成。
  5. SHARDING_FILTER + IXSCAN

    • 在分片集群中,通过 SHARDING_FILTER 阶段过滤掉不匹配分片键的数据,再结合索引扫描。
    • 场景:当查询条件包含分片键(如分片键为 user_id 时查询 db.collection.find({user_id: "U123", status: "paid"})),IXSCAN 会在各分片本地执行,SHARDING_FILTER 确保路由正确性。

注意事项

  • 避免出现低效阶段如 COLLSCAN(全集合扫描)或大量 SORT 内存排序。
  • 可通过 explain("executionStats") 分析实际执行计划,确认是否按预期使用索引。
  • 复合索引需遵循最左前缀原则(如索引 {a:1, b:1}{a:1} 有效,但对 {b:1} 无效)。在 MongoDB 查询优化中,合理的执行计划(execution plan)应该尽量使用索引扫描(IXSCAN)来提升查询性能。以下是针对普通查询期望看到的理想 stage 组合及其具体应用场景的详细说明:
  1. Fetch + IDHACK

    • 这是针对 _id 字段查询的最高效执行计划,直接通过主键(_id)定位文档。
    • 示例:查询 db.collection.find({_id: ObjectId("507f1f77bcf86cd799439011")}) 时,MongoDB 会直接使用 IDHACK 快速定位文档,再通过 Fetch 阶段获取完整文档内容。
  2. Fetch + IXSCAN

    • 通过普通索引快速定位文档位置,再获取完整文档数据。
    • 应用场景:对已创建索引的字段进行等值或范围查询(如 db.collection.find({age: {$gt: 25}})),IXSCAN 会先扫描索引条目,Fetch 阶段根据索引指针拉取完整文档。
    • 关键点:确保查询字段已正确创建索引(如 db.collection.createIndex({age: 1}))。
  3. Limit + (Fetch + IXSCAN)

    • 在索引扫描基础上结合 Limit 阶段,限制返回结果数量以提升性能。
    • 典型场景:分页查询(如 db.collection.find({status: "active"}).limit(10))。
    • 优化效果:Limit 会尽早截断数据流,避免不必要的 Fetch 操作。
  4. PROJECTION + IXSCAN

    • 通过覆盖查询(Covered Query)直接从索引中返回数据,无需访问文档。
    • 条件:查询的所有字段都包含在索引中,且未使用 _id 字段(或显式排除 {_id: 0})。
    • 示例:若索引为 {name: 1, age: 1},查询 db.collection.find({name: "Alice"}, {age: 1, _id: 0}) 会仅通过 IXSCAN 完成。
  5. SHARDING_FILTER + IXSCAN

    • 在分片集群中,通过 SHARDING_FILTER 阶段过滤掉不匹配分片键的数据,再结合索引扫描。
    • 场景:当查询条件包含分片键(如分片键为 user_id 时查询 db.collection.find({user_id: "U123", status: "paid"})),IXSCAN 会在各分片本地执行,SHARDING_FILTER 确保路由正确性。

注意事项

  • 避免出现低效阶段如 COLLSCAN(全集合扫描)或大量 SORT 内存排序。
  • 可通过 explain("executionStats") 分析实际执行计划,确认是否按预期使用索引。
  • 复合索引需遵循最左前缀原则(如索引 {a:1, b:1}{a:1} 有效,但对 {b:1} 无效)。

不希望看到的Stage

在MongoDB查询执行计划分析中,以下stage会严重影响查询性能,是我们在优化时需要重点关注的性能瓶颈:

1. COLLSCAN (全表扫描)

  • 问题描述:当查询无法使用索引时,MongoDB会执行全表扫描,逐行检查集合中的每个文档
  • 性能影响:随着数据量增长,性能呈线性下降
  • 典型场景
    • 查询条件未建立索引
    • 使用了无法使用索引的操作符(如$regex未使用前缀匹配)
    • 索引字段使用了表达式或函数
  • 优化方案
    • 为查询条件创建合适的索引
    • 避免使用无法利用索引的操作符
    • 使用explain()分析查询计划

2. SORT (未使用索引的排序)

  • 问题描述:当排序操作无法利用索引时,MongoDB需要在内存中排序
  • 性能影响
    • 消耗大量内存
    • 排序数据量大时会出现内存溢出
    • 可能导致磁盘临时文件写入
  • 典型场景
    • 排序字段未建立索引
    • 复合索引排序字段顺序不正确
    • 排序方向与索引方向不一致
  • 优化方案
    • 为排序字段创建索引
    • 确保复合索引字段顺序与查询顺序匹配
    • 考虑排序方向与索引方向一致

3. COUNT (未使用索引的计数)

  • 问题描述:当执行count操作且未使用索引时,MongoDB需要扫描整个集合
  • 性能影响
    • 数据量大时性能极差
    • 占用大量I/O资源
  • 典型场景
    • 简单count()未使用索引
    • count()带查询条件但条件字段无索引
  • 优化方案
    • 为常用计数字段创建索引
    • 考虑使用预估计数(countDocuments替代estimateDocumentCount)
    • 对大数据集考虑使用预聚合计数

注意:这些stage在某些特定场景下可能无法避免,但应该尽量减少其出现频率,特别是在高频查询和关键业务路径上。## 不希望看到的Stage

在MongoDB查询执行计划分析中,以下stage会严重影响查询性能,是我们在优化时需要重点关注的性能瓶颈:

1. COLLSCAN (全表扫描)

  • 问题描述:当查询无法使用索引时,MongoDB会执行全表扫描,逐行检查集合中的每个文档
  • 性能影响:随着数据量增长,性能呈线性下降
  • 典型场景
    • 查询条件未建立索引
    • 使用了无法使用索引的操作符(如$regex未使用前缀匹配)
    • 索引字段使用了表达式或函数
  • 优化方案
    • 为查询条件创建合适的索引
    • 避免使用无法利用索引的操作符
    • 使用explain()分析查询计划

2. SORT (未使用索引的排序)

  • 问题描述:当排序操作无法利用索引时,MongoDB需要在内存中排序
  • 性能影响
    • 消耗大量内存
    • 排序数据量大时会出现内存溢出
    • 可能导致磁盘临时文件写入
  • 典型场景
    • 排序字段未建立索引
    • 复合索引排序字段顺序不正确
    • 排序方向与索引方向不一致
  • 优化方案
    • 为排序字段创建索引
    • 确保复合索引字段顺序与查询顺序匹配
    • 考虑排序方向与索引方向一致

3. COUNT (未使用索引的计数)

  • 问题描述:当执行count操作且未使用索引时,MongoDB需要扫描整个集合
  • 性能影响
    • 数据量大时性能极差
    • 占用大量I/O资源
  • 典型场景
    • 简单count()未使用索引
    • count()带查询条件但条件字段无索引
  • 优化方案
    • 为常用计数字段创建索引
    • 考虑使用预估计数(countDocuments替代estimateDocumentCount)
    • 对大数据集考虑使用预聚合计数

注意:这些stage在某些特定场景下可能无法避免,但应该尽量减少其出现频率,特别是在高频查询和关键业务路径上。

posted @ 2025-11-14 09:07  clnchanpin  阅读(35)  评论(0)    收藏  举报