一、索引基础
1.1 什么是索引
索引是MongoDB中用于加速查询的数据结构,类似于书籍的目录。没有索引时,MongoDB必须扫描整个集合才能找到匹配的文档(全表扫描),而有了索引,MongoDB可以快速定位到目标文档,大幅减少查询时间。
1.2 索引类型
| 索引类型 | 描述 | 适用场景 |
|---|---|---|
| 单字段索引 | 对单个字段建立索引 | 简单查询条件 |
| 复合索引 | 对多个字段建立索引 | 多条件查询 |
| 多键索引 | 对数组字段建立索引 | 数组元素查询 |
| 哈希索引 | 对字段值哈希建立索引 | 等值查询 |
| 文本索引 | 对文本内容建立索引 | 全文搜索 |
| 地理空间索引 | 对坐标数据建立索引 | LBS服务 |
| 唯一索引 | 保证字段值唯一 | 主键、邮箱等 |
1.3 索引代价
索引并非越多越好,每个索引都有代价:
– 写入开销:每次插入/更新/删除都需要更新索引
– 存储空间:索引占用额外的磁盘空间
– 内存占用:活跃索引需要常驻内存
二、索引设计原则
2.1 ESR原则
MongoDB复合索引的黄金法则——Equality、Sort、Range:
1. 等值查询字段放前面:精确匹配的条件应放在最前
2. 排序字段放中间:需要排序的字段放在等值之后
3. 范围查询字段放后面:范围查询($gt, $lt等)放在最后
// 查询:status等值 + 按created_at排序 + age范围
// 正确索引
db.users.createIndex({ status: 1, created_at: -1, age: 1 })
// 错误索引(范围查询在排序前)
db.users.createIndex({ status: 1, age: 1, created_at: -1 })
2.2 选择性原则
索引的选择性越高,过滤效果越好:
– 高选择性:字段值唯一性高(如email、user_id)
– 低选择性:字段值重复多(如status、gender)
低选择性的字段不应单独建索引,应与其他字段组合。
2.3 覆盖查询
如果查询的所有字段都包含在索引中,MongoDB可以直接从索引返回结果,无需读取文档:
// 创建覆盖索引
db.users.createIndex({ email: 1, name: 1, phone: 1 })
// 覆盖查询(只返回索引包含的字段)
db.users.find(
{ email: "test@example.com" },
{ name: 1, phone: 1, _id: 0 }
).hint({ email: 1, name: 1, phone: 1 })
三、索引创建与优化
3.1 创建单字段索引
// 创建升序索引
db.users.createIndex({ email: 1 })
// 创建降序索引
db.users.createIndex({ created_at: -1 })
// 创建唯一索引
db.users.createIndex({ email: 1 }, { unique: true })
// 创建稀疏索引(仅索引存在该字段的文档)
db.users.createIndex({ nickname: 1 }, { sparse: true })
// 创建TTL索引(自动过期删除)
db.sessions.createIndex({ created_at: 1 }, { expireAfterSeconds: 3600 })
// 创建部分索引(仅索引满足条件的文档)
db.orders.createIndex(
{ status: 1, created_at: -1 },
{ partialFilterExpression: { status: { $in: ["active", "pending"] } } }
)
3.2 创建复合索引
// 基本复合索引
db.orders.createIndex({ user_id: 1, created_at: -1 })
// 指定索引名称
db.orders.createIndex(
{ user_id: 1, created_at: -1 },
{ name: "idx_user_orders" }
)
// 后台创建(不阻塞数据库操作)
db.orders.createIndex(
{ user_id: 1, created_at: -1 },
{ background: true }
)
3.3 创建文本索引
// 单字段文本索引
db.articles.createIndex({ content: "text" })
// 多字段文本索引
db.articles.createIndex({
title: "text",
content: "text",
tags: "text"
}, {
weights: {
title: 10,
content: 5,
tags: 2
},
name: "article_text_index"
})
// 全文搜索
db.articles.find({ $text: { $search: "MongoDB优化" } }, { score: { $meta: "textScore" } }).sort({ score: { $meta: "textScore" } })
3.4 创建地理空间索引
// 2d索引(平面坐标)
db.locations.createIndex({ location: "2d" })
// 2dsphere索引(球面坐标)
db.locations.createIndex({ location: "2dsphere" })
// 地理空间查询
db.locations.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [121.4737, 31.2304] },
$maxDistance: 5000
}
}
})
四、索引分析工具
4.1 explain()分析查询
// 查看查询计划
db.users.find({ email: "test@example.com" }).explain()
// 查看执行统计
db.users.find({ email: "test@example.com" }).explain("executionStats")
// 查看详细执行信息
db.users.find({ email: "test@example.com" }).explain("allPlansExecution")
关键字段解读:
// executionStats 关键指标
{
executionTimeMillis: 5, // 执行时间(ms)
totalDocsExamined: 1, // 扫描文档数
totalKeysExamined: 1, // 扫描索引键数
executionStages: {
stage: "IXSCAN", // 使用了索引扫描
indexName: "email_1", // 使用的索引名
docsExamined: 1,
keysExamined: 1
}
}
4.2 性能指标判断
| 指标 | 理想值 | 说明 |
|---|---|---|
| stage | IXSCAN/FETCH | 使用索引扫描 |
| totalKeysExamined ≈ nReturned | 接近1:1 | 索引效率高 |
| totalDocsExamined ≈ nReturned | 接近1:1 | 无多余扫描 |
| executionTimeMillis | <100ms | 查询速度快 |
| stage = COLLSCAN | ❌ | 全表扫描,需要加索引 |
4.3 $indexStats分析索引使用
// 查看所有索引的使用统计
db.users.aggregate([{ $indexStats: {} }])
// 输出示例
{
name: "email_1",
accesses: { ops: 15000, since: ISODate("2026-01-01T00:00:00Z") },
usageHost: "mongodb-server:27017"
}
4.4 慢查询分析
// 启用慢查询日志(>100ms)
db.setProfilingLevel(1, { slowms: 100 })
// 查看慢查询
db.system.profile.find().sort({ ts: -1 }).limit(10).pretty()
// 查看最慢的查询
db.system.profile.find({
millis: { $gt: 100 }
}).sort({ millis: -1 }).limit(10)
// 关闭慢查询日志
db.setProfilingLevel(0)
五、索引维护
5.1 查看索引信息
// 查看集合所有索引
db.users.getIndexes()
// 查看索引大小
db.users.stats().indexSizes
// 查看索引构建进度
db.currentOp({
$or: [
{ op: "command", "command.createIndexes": { $exists: true } },
{ op: "none", ns: /mydb/ }
]
})
5.2 删除无用索引
// 删除指定索引
db.users.dropIndex("email_1")
// 删除除_id外的所有索引
db.users.dropIndexes()
// 使用$indexStats识别无用索引
db.users.aggregate([{ $indexStats: {} }]).forEach(function(idx) {
if (idx.accesses.ops === 0) {
print("未使用索引: " + idx.name);
// db.users.dropIndex(idx.name); // 确认后删除
}
});
5.3 重建索引
// 重建集合所有索引
db.users.reIndex()
// 在副本集中滚动重建索引(不中断服务)
// 1. 在从节点停止
rs.stepDown()
// 2. 在从节点重建
db.users.reIndex()
// 3. 在其他从节点重复
六、高级优化技巧
6.1 部分索引(Partial Index)
仅索引满足条件的文档,减少索引大小:
// 只索引活跃用户
db.users.createIndex(
{ last_login: -1 },
{ partialFilterExpression: { status: "active" } }
)
// 只索引高价订单
db.orders.createIndex(
{ user_id: 1 },
{ partialFilterExpression: { amount: { $gt: 1000 } } }
)
6.2 稀疏索引(Sparse Index)
跳过不包含索引字段的文档:
// 只索引有手机号的用户
db.users.createIndex({ phone: 1 }, { sparse: true })
// 只索引有标签的文章
db.articles.createIndex({ tags: 1 }, { sparse: true })
6.3 隐藏索引(Hidden Index)
隐藏索引但不删除,用于测试删除索引的影响:
// 隐藏索引
db.users.hideIndex("email_1")
// 测试查询性能
db.users.find({ email: "test@example.com" }).explain("executionStats")
// 如果性能无影响,安全删除
db.users.dropIndex("email_1")
// 如果性能下降,恢复索引
db.users.unhideIndex("email_1")
6.4 索引交集优化
MongoDB可以使用多个索引的交集来满足查询:
// 单独索引
db.users.createIndex({ status: 1 })
db.users.createIndex({ created_at: -1 })
// 查询可以使用索引交集
db.users.find({
status: "active",
created_at: { $gt: ISODate("2026-01-01") }
}).explain("executionStats")
// 如果使用了AND_SORTED stage,说明使用了索引交集
七、索引优化实战
7.1 电商场景优化
// 商品查询(分类+价格范围+销量排序)
db.products.createIndex({ category: 1, price: 1, sales: -1 })
// 订单查询(用户+时间+状态)
db.orders.createIndex({ user_id: 1, created_at: -1, status: 1 })
// 搜索建议(前缀匹配)
db.products.createIndex({ name: 1 })
// 库存更新(SKU精确匹配)
db.inventory.createIndex({ sku: 1 }, { unique: true })
7.2 日志场景优化
// 日志查询(时间范围+级别)
db.logs.createIndex({ timestamp: -1, level: 1 })
// TTL索引自动清理
db.logs.createIndex({ timestamp: 1 }, { expireAfterSeconds: 2592000 }) // 30天
// 部分索引(只索引ERROR和WARN)
db.logs.createIndex(
{ timestamp: -1, level: 1 },
{ partialFilterExpression: { level: { $in: ["ERROR", "WARN"] } } }
)
7.3 社交场景优化
// 用户动态(用户+时间)
db.posts.createIndex({ user_id: 1, created_at: -1 })
// 好友关系(双向查询)
db.friends.createIndex({ user_id: 1, friend_id: 1 })
db.friends.createIndex({ friend_id: 1, user_id: 1 })
// 附近的人(地理空间)
db.users.createIndex({ location: "2dsphere" })
八、监控与告警
8.1 索引使用率监控脚本
#!/bin/bash
# /usr/local/bin/index-monitor.sh
DB_NAME="mydb"
OUTPUT="/tmp/index-report-$(date +%Y%m%d).txt"
echo "=== 索引使用率报告 - $(date) ===" > $OUTPUT
mongosh $DB_NAME --quiet --eval "
db.getCollectionNames().forEach(function(col) {
print('\n--- ' + col + ' ---');
db[col].aggregate([{\$indexStats: {}}]).forEach(function(idx) {
var ratio = idx.accesses.ops > 0 ? 'USED' : 'UNUSED';
print(idx.name + ' | ' + idx.accesses.ops + ' ops | ' + ratio);
});
});
" >> $OUTPUT
cat $OUTPUT
8.2 Prometheus监控指标
mongodb_index_accesses_total{collection="users", index="email_1"}
mongodb_index_size_bytes{collection="users", index="email_1"}
mongodb_collection_scan_ratio{collection="users"}
8.3 告警规则
# Prometheus告警规则
groups:
- name: mongodb_index
rules:
- alert: MongoDBHighCollScan
expr: rate(mongodb_collection_scan_ratio[5m]) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "MongoDB集合扫描比例过高"
总结
MongoDB索引优化是提升查询性能的核心手段,关键要点:
- 遵循ESR原则:等值→排序→范围,合理设计复合索引
- 定期审查索引:使用$indexStats识别无用索引并删除
- 善用explain():分析查询计划,确保使用正确的索引
- 控制索引数量:索引越多写入越慢,3-5个索引为佳
- 利用高级特性:部分索引、稀疏索引、隐藏索引减少开销
注:本文基于MongoDB 7.0和Debian 12编写,索引优化策略需根据实际业务场景调整。