MongoDB 查询分析(Query Analysis)完全指南
目标:找出慢查询 → 优化索引 → 提升性能 → 避免事故
一、查询分析核心流程(5 步法)
graph TD
A[发现慢查询] --> B[查看执行计划 explain()]
B --> C{是否命中索引?}
C -->|否| D[创建/优化索引]
C -->|是| E[检查扫描文档数]
E --> F{totalDocsExamined ≈ nReturned?}
F -->|否| G[优化查询/投影]
F -->|是| H[检查排序/分页]
H --> I{是否内存排序?}
I -->|是| J[添加排序索引]
I -->|否| K[性能已最优]
二、关键工具与命令
| 工具 | 用途 | 命令 |
|---|---|---|
explain() | 执行计划分析 | db.collection.find().explain("executionStats") |
system.profile | 慢查询日志 | db.system.profile.find() |
currentOp() | 实时运行查询 | db.currentOp() |
serverStatus() | 服务器状态 | db.serverStatus() |
| Atlas / PMM | 可视化监控 | 图形界面 |
三、explain() 输出详解(重点字段)
db.users.find({ age: 30 }).explain("executionStats")
1. queryPlanner.winningPlan
| 字段 | 含义 |
|---|---|
stage: "COLLSCAN" | 全表扫描 → 危险 |
stage: "IXSCAN" | 命中索引 → 好 |
indexName | 使用的索引名 |
2. executionStats
| 字段 | 含义 | 阈值 |
|---|---|---|
totalDocsExamined | 扫描文档数 | 应接近 nReturned |
totalKeysExamined | 扫描索引键数 | 越少越好 |
nReturned | 返回文档数 | 查询结果 |
executionTimeMillis | 执行时间 | > 100ms 需关注 |
3. 关键判断
| 条件 | 结论 |
|---|---|
totalDocsExamined >> nReturned | 索引无效 |
stage: "COLLSCAN" | 无索引 |
hasSortStage: true | 内存排序 → 慢 |
PROJECTION_COVERED | 覆盖索引 → 极快 |
四、慢查询日志(Profiler)
1. 开启慢查询记录
// 记录 > 100ms 的查询
db.setProfilingLevel(1, { slowms: 100 })
// 查看状态
db.getProfilingStatus()
2. 查询慢查询日志
// 最近 10 条慢查询
db.system.profile.find()
.sort({ ts: -1 })
.limit(10)
.pretty()
输出示例:
{
"op": "query",
"ns": "mydb.users",
"query": { "age": 30 },
"keysExamined": 0,
"docsExamined": 1000000,
"nreturned": 1,
"millis": 850 // 850ms!
}
五、实战案例分析
案例 1:全表扫描(COLLSCAN)
db.orders.find({ status: "pending" }).explain()
{
"stage": "COLLSCAN",
"totalDocsExamined": 5000000
}
问题:无索引
解决:
db.orders.createIndex({ status: 1 })
案例 2:索引存在但未覆盖
db.users.find(
{ age: { $gte: 25 } },
{ name: 1, email: 1, _id: 0 }
).explain()
{
"totalDocsExamined": 100000,
"totalKeysExamined": 100000
}
问题:回表
解决:创建覆盖索引
db.users.createIndex({ age: 1, name: 1, email: 1 })
案例 3:内存排序(SORT 阶段)
db.users.find().sort({ createdAt: -1 }).limit(10).explain()
{
"stage": "SORT",
"sortStage": { ... },
"totalDocsExamined": 1000000
}
问题:内存排序 → OOM 风险
解决:
db.users.createIndex({ createdAt: -1 })
六、索引优化黄金法则(ESR)
Equality → Sort → Range
// 查询:status="active" + 按 createdAt 降序 + age > 18
db.logs.createIndex({
status: 1, // 等值
createdAt: -1, // 排序
age: 1 // 范围
})
七、实时监控:db.currentOp()
// 查看运行 > 5 秒的查询
db.currentOp({
"secs_running": { $gt: 5 }
})
杀慢查询:
db.killOp(<opId>)
八、Node.js + Mongoose 分析
// 开启 Mongoose 调试
mongoose.set('debug', true);
// 或手动 explain
const result = await User.find({ age: 30 })
.select('name email')
.explain('executionStats');
九、生产级监控方案
| 工具 | 功能 |
|---|---|
| MongoDB Atlas Performance Advisor | 自动推荐索引 |
| Percona PMM | Grafana 仪表盘 |
| Prometheus + mongodb_exporter | 指标采集 |
| Ops Manager | 企业级监控 |
十、查询分析 Checklist
| 检查项 | 命令 |
|---|---|
| 是否命中索引? | IXSCAN |
| 扫描文档数? | totalDocsExamined |
| 是否回表? | totalDocsExamined > nReturned |
| 是否内存排序? | SORT 阶段 |
| 是否覆盖索引? | PROJECTION_COVERED |
| 慢查询记录? | db.system.profile |
十一、终极优化案例
// 原始慢查询(850ms)
db.orders.find({
status: "completed",
createdAt: { $gte: ISODate("2025-01-01") }
})
.sort({ total: -1 })
.skip(100)
.limit(20)
// 优化后(5ms)
db.orders.createIndex({
status: 1,
createdAt: -1,
total: -1
})
db.orders.find(
{ status: "completed", createdAt: { $gte: ISODate("2025-01-01") } },
{ _id: 0, orderId: 1, total: 1, customer: 1 }
)
.sort({ total: -1 })
.skip(100)
.limit(20)
.explain()
{
"totalDocsExamined": 20,
"totalKeysExamined": 120,
"stage": "PROJECTION_COVERED"
}
总结:查询分析黄金三问
| 问题 | 检查点 |
|---|---|
| 1. 有没有用索引? | IXSCAN vs COLLSCAN |
| 2. 扫了多少文档? | totalDocsExamined |
| 3. 是不是覆盖查询? | PROJECTION_COVERED + _id: 0 |
你现在可以:
// 一键分析任何查询
db.collection.find(...).explain("executionStats")
把你的慢查询贴给我,我 30 秒出优化方案:
- 查询语句(
find/aggregate) - 集合大小(文档数)
- 当前索引(
getIndexes())
回复 3 行代码,我生成:索引 + 优化查询 + explain 对比!