索引是数据库性能的核心。MongoDB 使用 B-Tree 索引,与关系型数据库类似,但有一些独特之处——如多键索引(数组索引)、文本索引、TTL 索引等。理解索引原理并正确使用 Explain 进行分析,是 MongoDB 调优的关键。
1. 索引基础
1.1 创建与管理
// 单键索引
db.users.createIndex({ email: 1 }); // 1 = 升序, -1 = 降序
// 复合索引(最左前缀原则)
db.orders.createIndex({ status: 1, createdAt: -1 });
// 唯一索引
db.users.createIndex({ email: 1 }, { unique: true });
// 查看索引
db.users.getIndexes();
db.users.totalIndexSize(); // 索引占用空间
// 删除索引
db.users.dropIndex("email_1");
db.users.dropIndexes();
1.2 索引对性能的影响
| 操作 | 无索引 | 有索引 |
|---|---|---|
| 查询 | 集合扫描 COLSCAN | 索引扫描 IXSCAN |
| 写入 | 快 | 慢(需更新索引) |
| 更新 | 慢 | 可能快(通过索引定位) |
| 内存 | 无额外占用 | 常驻内存 |
2. 索引类型
2.1 单键索引
// 最基础的索引
db.users.createIndex({ age: 1 });
// 适用查询:
db.users.find({ age: 30 });
db.users.find({ age: { $gte: 18, $lte: 60 } }).sort({ age: 1 });
2.2 复合索引
// 查询模式: WHERE status = ? AND createdAt > ? ORDER BY createdAt DESC
// 索引设计: { status: 1, createdAt: -1 }
db.orders.createIndex({ status: 1, createdAt: -1 });
// ✅ 有效使用:
db.orders.find({ status: "pending" });
db.orders.find({ status: "paid", createdAt: { $gt: yesterday } });
db.orders.find({ status: "pending" }).sort({ createdAt: -1 });
// ❌ 无法使用(缺少 status):
db.orders.find({ createdAt: { $gt: yesterday } });
// ❌ 无法排序(排序方向不符):
db.orders.find({ status: "pending" }).sort({ createdAt: 1 });
最左前缀原则:复合索引 {a, b, c} 可被以下查询使用:
{a},{a, b},{a, b, c}{a}加按b排序
2.3 多键索引(数组索引)
// 自动为数组字段创建多键索引
db.products.createIndex({ tags: 1 });
// 查询:
db.products.find({ tags: "electronics" }); // IXSCAN
db.products.find({ tags: { $all: ["electronics", "sale"] } });
// 限制: 复合索引中最多一个多键字段
db.orders.createIndex({ items: 1, status: 1 }); // ❌ 可能失败
2.4 文本索引
// 全文搜索索引
db.articles.createIndex({
title: "text",
content: "text",
tags: "text"
}, {
weights: {
title: 10, // 标题匹配权重更高
content: 5,
tags: 3
},
default_language: "chinese"
});
// 全文查询
db.articles.find({ $text: { $search: "MongoDB indexing" } },
{ score: { $meta: "textScore" } })
.sort({ score: { $meta: "textScore" } });
// 短语搜索
db.articles.find({ $text: { $search: "\"exact phrase\"" } });
2.5 TTL 索引
// 自动删除过期文档(适用于验证码、临时数据)
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600 } // 1小时过期
);
// TTL 后台线程每 60 秒运行一次,过期时间可能有延迟
2.6 部分索引与稀疏索引
// 稀疏索引: 只对存在该字段的文档建立索引
db.users.createIndex({ phone: 1 }, { sparse: true });
// 部分索引: 只对满足条件的文档建立索引(更有用)
db.orders.createIndex(
{ orderId: 1 },
{ partialFilterExpression: { status: { $eq: "pending" } } }
);
// 只为 pending 订单建立索引,大幅减少索引大小
2.7 其他索引类型
| 索引类型 | 用途 | 示例 |
|---|---|---|
| Hashed | 分片键哈希 | db.users.createIndex({ _id: "hashed" }) |
| Wildcard | 动态字段索引 | db.logs.createIndex({ "$**": 1 }) |
| Clustered | 按 _id 聚簇存储 | 仅在 MongoDB 5.0+ 时间序列集合支持 |
3. Explain 分析
3.1 执行计划关键字段
db.orders.find({ status: "pending" }).explain("executionStats");
{
"queryPlanner": {
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"indexName": "status_1_createdAt_-1",
"direction": "forward"
}
}
},
"executionStats": {
"nReturned": 150, // 返回文档数
"totalDocsExamined": 150, // 扫描文档数
"totalKeysExamined": 150, // 扫描索引键数
"executionTimeMillis": 2, // 执行时间
"executionStages": {
"stage": "IXSCAN",
"works": 151,
"advanced": 150,
"isEOF": 1
}
}
}
3.2 常见执行阶段
| Stage | 含义 | 是否可接受 |
|---|---|---|
COLLSCAN | 集合扫描 | 大数据量时 ❌ |
IXSCAN | 索引扫描 | ✅ |
FETCH | 取完整文档 | 需回表 |
PROJECTION_COVERED | 覆盖索引查询 | ✅ 最优 |
SORT | 内存排序 | 大数据量时 ❌ |
LIMIT | 限制返回数 | ✅ |
3.3 索引覆盖查询
// 索引 { _id: 1, status: 1 }
db.orders.find({ _id: 1 }, { status: 1, _id: 1 });
// Covered Query: 只从索引获取数据,不回表
// 执行计划中: "executionStats.totalDocsExamined" = 0
4. 查询优化技巧
| 技巧 | 说明 |
|---|---|
| 使用投影减少回表 | find({}, { needed: 1 }) |
| 利用覆盖索引 | 查询字段都在索引中 |
| $in 数量控制 | 单数组合并为一个 $in,但不超过 1000 个 |
| 避免 skip 深分页 | 使用最后文档的 _id 作为游标 |
| 排序使用索引 | 复合索引包含排序字段 |
| hint 强制索引 | 确认优化器选择错误时使用 |
5. 索引优化实战
5.1 ESR 原则(Equality, Sort, Range)
复合索引字段顺序决定查询效率,遵循 ESR 原则排序:
// 假设查询模式: WHERE status = ? AND category IN (?) AND createdAt > ? ORDER BY createdAt DESC
// ✅ ESR 顺序: Equality → Sort → Range
// Equality: status = "active"
// Sort: createdAt DESC
// Range: category IN [...]
db.orders.createIndex({ status: 1, createdAt: -1, category: 1 });
// ❌ 错误顺序: Range 在 Sort 前
db.orders.createIndex({ status: 1, category: 1, createdAt: -1 });
// 当 category IN 有多个值时,索引无法按 createdAt 有序返回
| 字段类型 | 示例 | 索引顺序建议 |
|---|---|---|
| Equality | status: "active" | 最前面 |
| Sort | ORDER BY createdAt | Equality 后面 |
| Range | age: { $gte: 18 } | 最末尾 |
| Multi-Value | tags: "electronics" | 避免放在 Sort 前 |
5.2 索引选择性分析
选择性 = 不同值数量 / 文档总数。选择性越高,索引效果越好:
// 分析字段选择性
db.users.aggregate([
{ $group: { _id: "$gender", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
])
// gender 只有两个值,选择性极低,单独索引无意义
db.users.aggregate([
{ $group: { _id: "$email", count: { $sum: 1 } } },
{ $count: "distinct_count" }
])
// email 几乎唯一,高选择性,适合单键索引
低选择性字段处理策略:放入复合索引前缀,与其他字段组合提高整体区分度。
5.3 索引交集与冗余索引清理
// 索引交集: { a: 1 } 和 { b: 1 } 可以联合服务 { a: 1, b: 1 }
// 但不保证有序,性能通常不如专用复合索引
// 查看冗余索引(MongoDB 4.4+)
db.orders.aggregate([
{ $indexStats: {} },
{
$project: {
name: 1,
key: 1,
accesses: "$accesses.ops",
since: "$accesses.since"
}
}
])
// 清理策略:
// 1. 前缀冗余: { a: 1, b: 1 } 已存在时,{ a: 1 } 通常冗余
// 2. 30天 0 访问的索引考虑删除
// 3. 用 db.currentOp() 观察索引构建对写入的影响
6. 大规模集合的索引管理
6.1 后台索引构建
前台构建会阻塞读写,生产环境必须使用后台模式:
// 后台创建(不会阻塞读写,但耗时更长)
db.large_collection.createIndex(
{ createdAt: -1 },
{ background: true }
)
// 监控进度(MongoDB 4.2+)
db.adminCommand({
"currentOp": true,
"$or": [
{ "desc": "Indexer" },
{ "type": "op", "originatingCommand.createIndexes": { $exists: true } }
]
})
// 4.2+ 支持 hybrid 构建:开始前台,达到阈值后自动切换后台
6.2 TTL 索引的注意事项
// TTL 单字段索引
db.sessions.createIndex(
{ lastAccessed: 1 },
{ expireAfterSeconds: 3600 }
)
// 限制:
// 1. TTL 字段必须是 Date 类型
db.sessions.insertOne({ lastAccessed: Date.now() }) // ❌ 不是 Date
// 2. 无法在复合索引上直接设置 TTL
// 变通:使用部分索引 + 应用层清理
db.sessions.createIndex(
{ status: 1, createdAt: 1 },
{
partialFilterExpression: {
status: { $eq: "pending" },
createdAt: { $lt: new Date(Date.now() - 3600 * 1000) }
}
}
)
// 3. 删除延迟:TTL 线程每 60 秒运行一次
db.serverStatus().metrics.ttl.deletedDocuments
6.3 分片环境下的索引
// 分片键前缀原则:分片键必须是索引前缀
// ✅ 正确:分片键 { userId: 1 },索引 { userId: 1, createdAt: -1 }
sh.shardCollection("mydb.orders", { userId: 1 })
// 全局唯一索引必须包含分片键
// ❌ 错误:无法直接创建 { email: 1 } 唯一索引
// ✅ 变通:{ email: 1, userId: 1 } 唯一索引
db.orders.createIndex(
{ email: 1, userId: 1 },
{ unique: true }
)
//hashed 分片键(均匀分布)
sh.shardCollection("mydb.logs", { _id: "hashed" })
7. 索引监控与调优工具
| 工具 | 命令 | 用途 |
|---|---|---|
| Profiler | db.setProfilingLevel(2) | 捕获慢查询 |
| Query Plan Cache | db.collection.getPlanCache().list() | 查看优化器缓存 |
| Index Stats | $indexStats | 索引使用情况 |
| mongostat | CLI 工具 | 实时性能监控 |
| mongotop | CLI 工具 | 集合级读写耗时 |
// 启用慢查询日志(>100ms)
db.setProfilingLevel(1, { slowms: 100 })
db.system.profile.find().sort({ ts: -1 }).limit(5)
// 强制清理计划缓存
// 升级到新版本后、删除索引后、大量数据导入后执行
db.orders.getPlanCache().clear()
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。