这是《MongoDB 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。
执行计划解释 MongoDB 如何执行查询:使用哪个索引、扫描多少文档、是否内存排序、返回多少结果。优化查询不能靠猜,应以 explain 输出为准。
10.1 基本用法
查询计划:
db.orders.find({
user_id: "u_1001",
status: "PAID"
}).explain()
详细执行统计:
db.orders.find({
user_id: "u_1001",
status: "PAID"
}).explain("executionStats")
所有计划输出:
db.orders.find({
user_id: "u_1001"
}).explain("allPlansExecution")
聚合计划:
db.orders.aggregate([
{ $match: { user_id: "u_1001" } },
{ $sort: { created_at: -1 } }
]).explain()
10.2 关键字段
常见字段:
| 字段 | 含义 |
|---|---|
stage |
执行阶段 |
winningPlan |
优化器选择的计划 |
rejectedPlans |
被拒绝计划 |
indexName |
使用索引 |
keysExamined |
索引条目扫描数 |
docsExamined |
文档扫描数 |
nReturned |
返回数 |
totalKeysExamined |
总索引扫描 |
totalDocsExamined |
总文档扫描 |
executionTimeMillis |
执行时间 |
10.3 常见阶段
| 阶段 | 说明 |
|---|---|
COLLSCAN |
集合扫描 |
IXSCAN |
索引扫描 |
FETCH |
回表取文档 |
PROJECTION_COVERED |
覆盖索引投影 |
SORT |
内存排序 |
SORT_KEY_GENERATOR |
排序键生成 |
LIMIT |
限制条数 |
SUBPLAN |
$or 子计划 |
GEO_NEAR |
地理距离 |
COLLSCAN 不一定是错误,小集合或后台任务可以接受;高频在线查询通常必须避免。
10.4 读取计划
示例:
winningPlan
FETCH
IXSCAN idx_user_time_status
判断:
- 是否使用预期索引;
keysExamined是否接近nReturned;docsExamined是否远大于nReturned;- 是否有
SORT; - 是否被覆盖索引覆盖;
- 执行时间是否集中在某个阶段。
10.5 扫描效率
理想比例:
nReturned ≈ keysExamined
示例:
| keysExamined | docsExamined | nReturned | 判断 |
|---|---|---|---|
| 100 | 100 | 100 | 高效 |
| 100000 | 100000 | 100 | 选择性差 |
| 100 | 100000 | 100 | 过滤后置 |
| 0 | 1000000 | 0 | 集合扫描 |
索引选择性差时,调整字段顺序或查询条件比强制提示更可靠。
10.6 排序计划
无索引排序:
SORT
FETCH
IXSCAN idx_user
有索引排序:
FETCH
IXSCAN idx_user_created
排序阶段可能触发内存限制。分页和列表查询应尽量让排序字段进入复合索引。
10.7 覆盖查询
集合:
db.users.insertOne({
_id: "u_1001",
name: "Alice",
level: "GOLD"
})
索引:
db.users.createIndex({ user_id: 1, status: 1, created_at: -1 })
覆盖查询需要查询字段和投影字段都在索引中。覆盖查询可以减少 FETCH,但不要为了覆盖所有场景建立过多索引。
10.8 查询计划缓存
MongoDB 会缓存查询计划,数据分布变化后可能重新评估。查看:
db.orders.getPlanCache().list()
清理:
db.orders.getPlanCache().clear()
不要把清理缓存当成常规优化。应优先修正索引和查询。
10.9 索引提示
强制使用索引:
db.orders.find({
user_id: "u_1001",
status: "PAID"
}).hint("idx_user_time_status")
提示可以验证索引收益,但长期使用会让计划失去适应性。索引结构变化后,提示可能导致查询失败或退化。
10.10 生产分析流程
find slow query
-> read explain
-> identify scan / sort stage
-> inspect indexes
-> design new index
-> verify executionStats
-> deploy during low peak
-> monitor latency and write cost
上线新索引时要注意后台构建策略、副本集影响和磁盘空间。
10.11 常见计划问题
| 现象 | 可能原因 |
|---|---|
| COLLSCAN | 无索引或条件不满足前缀 |
| 大量 docsExamined | 索引选择性差 |
| SORT | 排序字段不在索引 |
| rejectedPlans 多 | 索引重复或冲突 |
| 计划变化 | 数据分布或缓存变化 |
| 聚合慢 | $match 后置或数据放大 |
本章小结
执行计划把查询性能问题变成可观察的证据。重点看 winningPlan、扫描数量、返回数量和排序阶段。查询优化应先减少输入数据,再优化索引顺序和排序,最后考虑缓存与提示。
思考题
COLLSCAN一定有问题吗?keysExamined和nReturned的关系说明什么?- 为什么
SORT阶段需要关注内存? - 覆盖索引的代价是什么?
- 为什么不建议长期使用
hint?