|
| 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