索引是数据库性能的核心。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 强制索引 | 确认优化器选择错误时使用 |
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。