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 // 可选参数,是否创建唯一索引
}
);
使用说明:
- 后台索引构建不会阻塞数据库的其他操作,适合在生产环境使用
- 相比前台索引创建,后台索引构建速度较慢但影响较小
- 可以在已有数据的集合上创建索引
- 可以同时指定多个字段创建复合索引
应用场景示例:
- 在用户集合上为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"}
)
注意事项:
- 后台索引构建期间,查询可能无法使用新索引
- 可以使用getIndexes()查看索引创建进度
- 大量数据时索引构建可能需要较长时间
- 可能需要额外的磁盘空间临时存储索引数据
获取针对某个集合的索引
在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() 方法是一个强大的查询分析工具,它接受不同的参数来控制返回信息的详细程度。通过设置不同的参数,我们可以查看不同层次的查询执行计划细节:
queryPlanner 模式 (默认参数)
- 这是最基本的执行计划分析模式
- 返回的信息包括:
- 查询优化器选择的获胜计划(winningPlan)
- 被拒绝的候选计划(rejectedPlans)
- 索引使用情况
- 查询阶段(如 COLLSCAN 或 IXSCAN)
- 示例:
db.collection.find({name: "John"}).explain("queryPlanner")
executionStats 模式
- 提供实际的执行统计信息
- 包含的信息有:
- 执行时间(executionTimeMillis)
- 扫描文档数量(nReturned/totalDocsExamined)
- 索引使用效率(keysExamined)
- 阶段执行时间(executionStages)
- 在某些 MongoDB 版本中,这个参数与 allPlanExecution 功能相同
- 示例:
db.collection.find({age: {$gt: 30}}).explain("executionStats")
allPlansExecution 模式
- 最详细的执行计划分析模式
- 返回所有候选计划的执行统计信息
- 包含的内容与 executionStats 基本相同,但额外提供:
- 所有候选计划的执行统计(allPlansExecution)
- 查询优化器的决策过程
- 常用于复杂的查询优化场景
- 示例:
db.collection.find({status: "active", score: {$gt: 80}}).explain("allPlansExecution")
这些参数的选择取决于具体的分析需求:
- 对于简单的查询优化,queryPlanner 通常足够
- 当需要了解查询实际执行性能时,应使用 executionStats
- 对于复杂查询或需要比较不同执行计划时,allPlansExecution 最为合适
注意:不同 MongoDB 版本可能在参数行为和返回字段上有细微差异,使用时需参考对应版本的官方文档。


executionStats

返回逐层分析
在 MongoDB 的 explain() 执行计划分析中,executionTimeMillis 是最直观的性能指标之一,它反映了查询语句的实际执行时间。这个数值当然是越小越好,表明查询效率越高。具体来说,explain() 返回结果中会包含三个不同层级的 executionTimeMillis 值:
executionStats.executionTimeMillis
这是整个查询语句的完整执行时间,从查询开始到返回结果的总耗时。它包括了查询准备、索引扫描、数据检索等所有环节的时间总和。例如,一个查询如果显示该值为 50ms,表示从发送查询到获取结果共花费了 50 毫秒。executionStats.executionStages.executionTimeMillisEstimate
这个值表示查询在检索 documents 并获取数据阶段所花费的时间。它反映了 MongoDB 从存储引擎中读取符合条件数据的时间开销。比如在这个阶段花费了 30ms,说明数据检索环节占用了较大部分查询时间。executionStats.executionStages.inputStage.executionTimeMillisEstimate
这个指标表示查询使用索引(如果存在)扫描所花费的时间。它反映了 MongoDB 在索引结构中定位符合条件数据的时间。例如,如果该值为 10ms,说明索引扫描环节较为高效。
在实际性能调优中,这三个时间值的关系通常会呈现:
- 当索引扫描时间(inputStage)占比较大时,可能需要优化索引结构
- 当数据检索时间(executionStages)过长时,可能需要考虑数据分片或优化查询条件
- 整体时间(executionTimeMillis)是评估查询性能的最终标准
典型的应用场景:
- 发现查询缓慢时,首先检查 executionTimeMillis 确认整体耗时
- 然后对比三个时间值,找出性能瓶颈所在环节
- 根据具体的时间分布情况采取相应的优化措施
在 MongoDB 查询性能分析的第二层,index(索引)与 document(文档)的扫描数量是影响查询效率的关键指标。这里我们重点讨论三个核心返回参数:
nReturned:表示查询实际返回的文档数量,即结果集大小。例如,当执行
db.users.find({age: 25}).limit(10)时,nReturned 最大值为10。totalKeysExamined:表示查询过程中扫描的索引条目数量。假设在
age字段上有索引:- 理想情况下,如果索引能精确定位到目标文档,如
{age: 25}的查询,可能只需要扫描少量索引键 - 糟糕情况下,如范围查询
{age: {$gt: 20}}可能需要扫描大量索引键
- 理想情况下,如果索引能精确定位到目标文档,如
totalDocsExamined:表示查询过程中扫描的完整文档数量。当出现以下情况时该值会较高:
- 查询无法使用索引(全表扫描)
- 虽然使用索引但需要回表获取完整文档(如查询包含非索引字段)
- 使用低效的索引(如区分度低的索引)
这些指标直接影响 executionTimeMillis(查询执行时间),扫描的文档和索引条目越少,查询速度就越快。以一个电商商品查询为例:
// 商品表有100万文档,category和price字段有复合索引
db.products.find({
category: "electronics",
price: {$gt: 500}
})
理想查询状态应满足:
nReturned = totalKeysExamined = totalDocsExamined = 实际匹配的商品数量
这意味着:
- 索引被完美利用,直接定位到目标文档
- 没有不必要的索引或文档扫描
- 查询只需读取必要的索引和文档数据
实际优化时,可以通过 explain("executionStats") 查看这些指标,并针对性地:
- 创建合适的索引
- 优化查询条件
- 使用覆盖索引
- 调整索引顺序
来尽可能接近这个理想状态。
第三层,stage状态分析,详细解析MongoDB查询执行计划中的各种stage类型及其应用场景:
COLLSCAN(全表扫描):
- 最基础的查询方式,会扫描集合中的每个文档
- 性能最低,当查询条件没有可用索引时会触发
- 示例:对没有索引的字段进行查询:
db.users.find({age: 25})
IXSCAN(索引扫描):
- 使用索引来定位文档
- 性能较好,是优化的目标
- 示例:对已建立索引的字段查询:
db.users.find({username: "john"}).explain()
FETCH(文档获取):
- 根据索引指针获取完整文档
- 通常出现在IXSCAN之后
- 示例:使用索引查询后获取完整文档的阶段
SHARD_MERGE(分片合并):
- 在分片集群中合并来自不同分片的结果
- 只出现在分片集群环境中
- 示例:跨分片查询:
db.users.find().explain()
SORT(内存排序):
- 在内存中对结果进行排序
- 可能消耗大量内存
- 示例:
db.users.find().sort({age: 1}).explain()
LIMIT(结果限制):
- 限制返回结果数量
- 示例:
db.users.find().limit(10).explain()
SKIP(结果跳过):
- 跳过指定数量的文档
- 示例:
db.users.find().skip(5).explain()
IDHACK(ID直接查询):
- 直接使用_id字段进行查询的优化路径
- 性能最好
- 示例:
db.users.find({_id: ObjectId("...")}).explain()
SHARDING_FILTER(分片过滤):
- 过滤不符合分片键条件的文档
- 防止查询不必要分片
- 示例:按分片键查询时出现
COUNT(计数运算):
- 执行计数操作
- 示例:
db.users.find().count().explain()
TEXT(全文检索):
- 使用全文索引进行文本搜索
- 需要预先创建文本索引
- 示例:
db.articles.find({$text: {$search: "mongodb"}}).explain()
PROJECTION(字段投影):
- 限定返回字段
- 可以减少网络传输和提高性能
- 示例:
db.users.find({}, {name: 1, age: 1}).explain()
在实际查询优化中,这些stage会以树形结构组合出现,例如一个典型的优化查询可能呈现为:IXSCAN → FETCH → PROJECTION → LIMIT。通过分析这些stage的组合和顺序,可以深入了解查询的性能特征和优化空间。
希望看到的Stage
在 MongoDB 查询优化中,合理的执行计划(execution plan)应该尽量使用索引扫描(IXSCAN)来提升查询性能。以下是针对普通查询期望看到的理想 stage 组合及其具体应用场景的详细说明:
Fetch + IDHACK
- 这是针对
_id字段查询的最高效执行计划,直接通过主键(_id)定位文档。 - 示例:查询
db.collection.find({_id: ObjectId("507f1f77bcf86cd799439011")})时,MongoDB 会直接使用IDHACK快速定位文档,再通过Fetch阶段获取完整文档内容。
- 这是针对
Fetch + IXSCAN
- 通过普通索引快速定位文档位置,再获取完整文档数据。
- 应用场景:对已创建索引的字段进行等值或范围查询(如
db.collection.find({age: {$gt: 25}})),IXSCAN会先扫描索引条目,Fetch阶段根据索引指针拉取完整文档。 - 关键点:确保查询字段已正确创建索引(如
db.collection.createIndex({age: 1}))。
Limit + (Fetch + IXSCAN)
- 在索引扫描基础上结合
Limit阶段,限制返回结果数量以提升性能。 - 典型场景:分页查询(如
db.collection.find({status: "active"}).limit(10))。 - 优化效果:
Limit会尽早截断数据流,避免不必要的Fetch操作。
- 在索引扫描基础上结合
PROJECTION + IXSCAN
- 通过覆盖查询(Covered Query)直接从索引中返回数据,无需访问文档。
- 条件:查询的所有字段都包含在索引中,且未使用
_id字段(或显式排除{_id: 0})。 - 示例:若索引为
{name: 1, age: 1},查询db.collection.find({name: "Alice"}, {age: 1, _id: 0})会仅通过IXSCAN完成。
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 组合及其具体应用场景的详细说明:
Fetch + IDHACK
- 这是针对
_id字段查询的最高效执行计划,直接通过主键(_id)定位文档。 - 示例:查询
db.collection.find({_id: ObjectId("507f1f77bcf86cd799439011")})时,MongoDB 会直接使用IDHACK快速定位文档,再通过Fetch阶段获取完整文档内容。
- 这是针对
Fetch + IXSCAN
- 通过普通索引快速定位文档位置,再获取完整文档数据。
- 应用场景:对已创建索引的字段进行等值或范围查询(如
db.collection.find({age: {$gt: 25}})),IXSCAN会先扫描索引条目,Fetch阶段根据索引指针拉取完整文档。 - 关键点:确保查询字段已正确创建索引(如
db.collection.createIndex({age: 1}))。
Limit + (Fetch + IXSCAN)
- 在索引扫描基础上结合
Limit阶段,限制返回结果数量以提升性能。 - 典型场景:分页查询(如
db.collection.find({status: "active"}).limit(10))。 - 优化效果:
Limit会尽早截断数据流,避免不必要的Fetch操作。
- 在索引扫描基础上结合
PROJECTION + IXSCAN
- 通过覆盖查询(Covered Query)直接从索引中返回数据,无需访问文档。
- 条件:查询的所有字段都包含在索引中,且未使用
_id字段(或显式排除{_id: 0})。 - 示例:若索引为
{name: 1, age: 1},查询db.collection.find({name: "Alice"}, {age: 1, _id: 0})会仅通过IXSCAN完成。
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在某些特定场景下可能无法避免,但应该尽量减少其出现频率,特别是在高频查询和关键业务路径上。
浙公网安备 33010602011771号