Skip to content

Commit 59c7fa9

Browse files
committed
add explain
1 parent b5fa4e5 commit 59c7fa9

5 files changed

Lines changed: 749 additions & 52 deletions

File tree

‎docs/EXPLAIN.md‎

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
# Explain 功能文档
2+
3+
## 概述
4+
5+
`explain` 功能允许您查看 MongoDB 查询的执行计划,而不实际执行查询。这对于查询优化、索引分析和性能调优非常有用。
6+
7+
## 支持的方法
8+
9+
以下方法均支持 `explain` 参数:
10+
11+
- ✅ `findOne()`
12+
- ✅ `find()`
13+
- ✅ `stream()` (通过 `find()` 实现)
14+
- ✅ `count()`
15+
- ✅ `aggregate()`
16+
- ✅ `distinct()`
17+
18+
## 使用方法
19+
20+
### 基础用法
21+
22+
```javascript
23+
// 返回 queryPlanner 级别的执行计划
24+
const explain = await collection.findOne({
25+
query: { age: { $gt: 25 } },
26+
explain: true
27+
});
28+
```
29+
30+
### Verbosity 级别
31+
32+
`explain` 参数支持以下级别:
33+
34+
| 级别 | 说明 | 返回内容 |
35+
|------|------|----------|
36+
| `true` 或 `'queryPlanner'` | 查询计划器(最快) | 仅返回查询计划,不执行查询 |
37+
| `'executionStats'` | 执行统计(推荐) | 返回查询计划 + 实际执行统计 |
38+
| `'allPlansExecution'` | 所有计划执行(最详细) | 返回所有候选计划的详细执行信息 |
39+
40+
## 示例
41+
42+
### 1. findOne - 查看单条查询计划
43+
44+
```javascript
45+
// 基础 explain
46+
const explain = await users.findOne({
47+
query: { email: 'user@example.com' },
48+
explain: true
49+
});
50+
console.log(explain.queryPlanner.winningPlan);
51+
52+
// 查看执行统计
53+
const explainStats = await users.findOne({
54+
query: { age: { $gte: 18 } },
55+
sort: { createdAt: -1 },
56+
explain: 'executionStats'
57+
});
58+
console.log(`扫描文档数: ${explainStats.executionStats.totalDocsExamined}`);
59+
console.log(`返回文档数: ${explainStats.executionStats.nReturned}`);
60+
```
61+
62+
### 2. find - 查看多条查询计划
63+
64+
```javascript
65+
// 分析复杂查询
66+
const explain = await users.find({
67+
query: {
68+
status: 'active',
69+
age: { $gte: 18, $lte: 65 },
70+
city: { $in: ['Beijing', 'Shanghai'] }
71+
},
72+
sort: { score: -1 },
73+
limit: 100,
74+
explain: 'executionStats'
75+
});
76+
77+
// 检查是否使用了索引
78+
if (explain.executionStats.executionStages.stage === 'COLLSCAN') {
79+
console.warn('⚠️ 警告:查询使用了全表扫描,建议添加索引');
80+
}
81+
```
82+
83+
### 3. count - 查看计数查询计划
84+
85+
```javascript
86+
// 带条件的 count
87+
const explainCount = await users.count({
88+
query: { status: 'active', age: { $gte: 18 } },
89+
explain: 'executionStats'
90+
});
91+
92+
// 空查询的 count (estimatedDocumentCount)
93+
const explainEmpty = await users.count({
94+
explain: true
95+
});
96+
// 注意:空查询使用 estimatedDocumentCount,explain 返回简化信息
97+
```
98+
99+
### 4. aggregate - 查看聚合管道计划
100+
101+
```javascript
102+
const explain = await orders.aggregate([
103+
{ $match: { status: 'completed', date: { $gte: new Date('2024-01-01') } } },
104+
{ $group: {
105+
_id: '$userId',
106+
totalAmount: { $sum: '$amount' },
107+
orderCount: { $sum: 1 }
108+
}},
109+
{ $sort: { totalAmount: -1 } },
110+
{ $limit: 10 }
111+
], {
112+
explain: 'executionStats'
113+
});
114+
115+
// 查看每个阶段的性能
116+
explain.stages?.forEach((stage, i) => {
117+
console.log(`Stage ${i}:`, stage);
118+
});
119+
```
120+
121+
### 5. distinct - 查看去重查询计划
122+
123+
```javascript
124+
const explain = await users.distinct('city', {
125+
query: { status: 'active' },
126+
explain: 'executionStats'
127+
});
128+
129+
// distinct 通过聚合管道模拟
130+
// 实际执行的是: [{ $match: {...} }, { $group: { _id: '$city' } }]
131+
```
132+
133+
## 性能分析指南
134+
135+
### 关键指标
136+
137+
从 `executionStats` 中关注以下指标:
138+
139+
```javascript
140+
const stats = explain.executionStats;
141+
142+
// 1. 执行时间
143+
console.log(`执行时间: ${stats.executionTimeMillis}ms`);
144+
145+
// 2. 扫描文档数 vs 返回文档数
146+
console.log(`扫描: ${stats.totalDocsExamined}, 返回: ${stats.nReturned}`);
147+
// 比例越接近 1:1 越好
148+
149+
// 3. 是否使用索引
150+
const stage = stats.executionStages.stage;
151+
if (stage === 'IXSCAN') {
152+
console.log('✅ 使用了索引扫描');
153+
} else if (stage === 'COLLSCAN') {
154+
console.log('⚠️ 使用了全表扫描');
155+
}
156+
157+
// 4. 索引键检查数
158+
console.log(`索引键扫描: ${stats.totalKeysExamined}`);
159+
```
160+
161+
### 性能优化建议
162+
163+
| 现象 | 原因 | 建议 |
164+
|------|------|------|
165+
| `totalDocsExamined >> nReturned` | 扫描了大量无关文档 | 添加或优化索引 |
166+
| `stage === 'COLLSCAN'` | 全表扫描 | 为查询字段创建索引 |
167+
| `executionTimeMillis > 100ms` | 查询慢 | 检查索引、查询条件、排序 |
168+
| `totalKeysExamined >> nReturned` | 索引不够精确 | 优化索引字段顺序 |
169+
170+
## 注意事项
171+
172+
### 1. explain 模式下的行为
173+
174+
- ✅ 返回查询执行计划(对象)
175+
- ❌ 不返回实际查询结果(文档)
176+
- ❌ 结果不会被缓存
177+
- ❌ 不触发慢查询日志
178+
179+
### 2. 互斥选项
180+
181+
```javascript
182+
// ❌ explain 与 stream 互斥
183+
await collection.find({
184+
query: { status: 'active' },
185+
stream: true, // 将被忽略
186+
explain: true // explain 优先
187+
});
188+
189+
// ❌ explain 与 cache 互斥(explain 结果不缓存)
190+
await collection.find({
191+
query: { status: 'active' },
192+
cache: 60000, // 将被忽略
193+
explain: true
194+
});
195+
```
196+
197+
### 3. 特殊情况
198+
199+
**count 空查询:**
200+
```javascript
201+
// 使用 estimatedDocumentCount,没有实际的 explain
202+
const explain = await collection.count({ explain: true });
203+
// 返回简化的元信息而非完整的执行计划
204+
```
205+
206+
**distinct 实现:**
207+
```javascript
208+
// distinct 通过聚合管道模拟来获取 explain
209+
// 内部转换为: [{ $match: query }, { $group: { _id: '$field' } }]
210+
```
211+
212+
## 实战案例
213+
214+
### 案例 1:发现缺失索引
215+
216+
```javascript
217+
const explain = await users.find({
218+
query: { email: 'test@example.com' },
219+
explain: 'executionStats'
220+
});
221+
222+
if (explain.executionStats.totalDocsExamined > 1000) {
223+
console.warn('⚠️ 扫描了超过 1000 个文档,建议为 email 字段创建索引');
224+
// 创建索引: db.users.createIndex({ email: 1 })
225+
}
226+
```
227+
228+
### 案例 2:优化排序查询
229+
230+
```javascript
231+
const explain = await orders.find({
232+
query: { status: 'pending' },
233+
sort: { createdAt: -1 },
234+
limit: 20,
235+
explain: 'executionStats'
236+
});
237+
238+
// 检查是否需要内存排序
239+
if (explain.executionStats.executionStages.sortPattern) {
240+
console.warn('⚠️ 需要内存排序,建议创建复合索引: { status: 1, createdAt: -1 }');
241+
}
242+
```
243+
244+
### 案例 3:对比不同索引策略
245+
246+
```javascript
247+
// 策略 1: 单字段索引
248+
const explain1 = await users.find({
249+
query: { age: { $gte: 18 }, city: 'Beijing' },
250+
hint: { age: 1 },
251+
explain: 'executionStats'
252+
});
253+
254+
// 策略 2: 复合索引
255+
const explain2 = await users.find({
256+
query: { age: { $gte: 18 }, city: 'Beijing' },
257+
hint: { age: 1, city: 1 },
258+
explain: 'executionStats'
259+
});
260+
261+
// 对比性能
262+
console.log('单字段索引耗时:', explain1.executionStats.executionTimeMillis, 'ms');
263+
console.log('复合索引耗时:', explain2.executionStats.executionTimeMillis, 'ms');
264+
```
265+
266+
## 相关资源
267+
268+
- [MongoDB Explain 官方文档](https://www.mongodb.com/docs/manual/reference/method/cursor.explain/)
269+
- [查询计划器文档](https://www.mongodb.com/docs/manual/core/query-plans/)
270+
- [索引优化指南](https://www.mongodb.com/docs/manual/core/indexes/)
271+
272+
## 测试
273+
274+
运行测试以验证 explain 功能:
275+
276+
```bash
277+
# 运行单元测试
278+
node test/explain-test.js
279+
280+
# 运行演示示例
281+
node examples/explain-demo.js
282+
```
283+

0 commit comments

Comments
 (0)