第七章:索引与性能优化
最后更新: 2024-01-01
作者: MongoDB Team
页面目录
第七章:索引与性能优化
MongoDB 索引策略与查询性能调优
7.1 索引概述
┌─────────────────────────────────────────────────────────────────┐
│ 有索引 vs 无索引 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 无索引查询 (全表扫描) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Doc1 │ Doc2 │ Doc3 │ Doc4 │ Doc5 │ Doc6 │ ... │ DocN │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ 逐个检查 │
│ │
│ 有索引查询 (B-Tree 查找) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Index: [A]→[Doc] [B]→[Doc] [C]→[Doc] [D]→[Doc] ... │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ 直接定位 │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2 索引类型
单字段索引
// 在单个字段上创建索引
db.users.createIndex({ username: 1 }) // 升序
db.users.createIndex({ email: -1 }) // 降序
db.users.createIndex({ created_at: -1 }) // 降序
// 后台创建索引(不阻塞其他操作)
db.users.createIndex({ username: 1 }, { background: true })
// 唯一索引
db.users.createIndex({ email: 1 }, { unique: true })
// 稀疏索引(只索引存在该字段的文档)
db.users.createIndex({ phone: 1 }, { sparse: true })
复合索引
// 创建复合索引
db.orders.createIndex({ status: 1, created_at: -1 })
// 复合索引字段顺序很重要
// 查询 { status: "completed", created_at: { $gte: ... } }
// 索引 { status: 1, created_at: -1 } ✓ 高效
// 索引 { created_at: -1, status: 1 } ✗ 无法利用
// 查询 { status: "completed" }
// 以上两个索引都可以使用
// 查询 { created_at: { $gte: ... } }
// 只有 { created_at: -1, status: 1 } 可以使用
多键索引
// 为数组字段创建多键索引
db.products.createIndex({ tags: 1 })
// 索引数组中的元素
db.products.find({ tags: "electronics" })
// 多键索引不能支持某些操作
// 如:整个数组的比较
文本索引
// 单字段文本索引
db.articles.createIndex({ content: "text" })
// 多字段文本索引
db.articles.createIndex({
title: "text",
content: "text",
tags: "text"
})
// 带权重的文本索引
db.articles.createIndex(
{ title: "text", content: "text" },
{ weights: { title: 10, content: 1 } }
)
// 指定语言
db.articles.createIndex(
{ title: "text" },
{ default_language: "chinese" }
)
地理空间索引
// 2dsphere 索引(地球球面)
db.locations.createIndex({ location: "2dsphere" })
// 2d 索引(平面)
db.locations.createIndex({ coordinates: "2d" })
// 地理空间索引选项
db.locations.createIndex(
{ location: "2dsphere" },
{ bits: 26, min: -180, max: 180 }
)
哈希索引
// 哈希索引(用于分片)
db.events.createIndex({ timestamp: "hashed" })
// 哈希索引只支持等值查询
db.events.find({ timestamp: ISODate("2024-01-01") })
7.3 索引属性
唯一索引
// 唯一索引
db.users.createIndex({ email: 1 }, { unique: true })
// 复合唯一索引
db.user_sessions.createIndex(
{ user_id: 1, session_id: 1 },
{ unique: true }
)
// 唯一索引 + 稀疏
db.users.createIndex(
{ phone: 1 },
{ unique: true, sparse: true }
)
稀疏索引
// 稀疏索引不索引 null 值
db.users.createIndex({ phone: 1 }, { sparse: true })
// 非稀疏索引会索引 null
db.users.createIndex({ phone: 1 })
TTL 索引
// TTL 索引:自动删除过期文档
db.sessions.createIndex(
{ created_at: 1 },
{ expireAfterSeconds: 3600 } // 1 小时后删除
)
// 支持不同的时间精度
db.events.createIndex(
{ timestamp: 1 },
{ expireAfterSeconds: 0 } // 在指定时间删除
)
// 设置删除时间
db.events.insertOne({
event_type: "scheduled",
execute_at: ISODate("2024-01-15T10:00:00Z")
})
部分索引
// 只索引满足条件的文档
db.orders.createIndex(
{ customer_id: 1, created_at: -1 },
{
partialFilterExpression: {
status: { $in: ["pending", "processing"] }
}
}
)
// 部分索引应用场景
// 1. 只需要索引活跃用户
db.users.createIndex(
{ last_active: 1 },
{
partialFilterExpression: { status: "active" }
}
)
// 2. 只索引大文档
db.logs.createIndex(
{ timestamp: 1 },
{
partialFilterExpression: { level: { $in: ["ERROR", "CRITICAL"] } }
}
)
案例索引
// 忽略大小写的索引
db.users.createIndex(
{ username: 1 },
{ collation: { locale: "en", strength: 2 } }
)
// 查询时指定 collation
db.users.find({ username: "ALICE" }).collation({ locale: "en", strength: 2 })
7.4 索引管理
查看索引
// 查看集合的所有索引
db.users.getIndexes()
// 查看索引详细信息
db.users.getIndexSpecs()
// 查看索引大小
db.users.totalIndexSize()
删除索引
// 删除单个索引(通过字段名)
db.users.dropIndex({ username: 1 })
// 删除单个索引(通过索引名)
db.users.dropIndex("username_1")
// 删除所有非 _id 索引
db.users.dropIndexes()
// 删除除 _id 外的所有索引
db.users.getIndexes().forEach(idx => {
if (idx.name !== "_id_") {
db.users.dropIndex(idx.name)
}
})
重命名索引
// 通过重新创建删除来重命名
db.users.dropIndex("old_name")
db.users.createIndex({ username: 1 }, { name: "new_name" })
7.5 查询计划分析
explain()
// 查看查询计划
db.users.find({ email: "alice@example.com" }).explain()
// 查看执行统计
db.users.find({ email: "alice@example.com" }).explain("executionStats")
// 查看所有计划信息
db.users.find({ email: "alice@example.com" }).explain("allPlansExecution")
解释计划输出
db.users.find({ email: "alice@example.com" }).explain("executionStats")
输出示例:
{
queryPlanner: {
plannerVersion: 1,
namespace: "mydb.users",
indexFilterSet: false,
parsedQuery: { email: { $eq: "alice@example.com" } },
winningPlan: {
stage: "FETCH", // 执行阶段
inputStage: {
stage: "IXSCAN", // 索引扫描
keyPattern: { email: 1 },
indexName: "email_1",
isMultiKey: false,
direction: "forward",
indexBounds: {
email: ["[\"alice@example.com\", \"alice@example.com\"]"]
}
}
},
rejectedPlans: [] // 被拒绝的执行计划
},
executionStats: {
executionSuccess: true,
nReturned: 1, // 返回文档数
executionTimeMillis: 0, // 执行时间(毫秒)
totalKeysExamined: 1, // 扫描的索引键数
totalDocsExamined: 1, // 扫描的文档数
executionStages: { ... }
}
}
执行阶段说明
| Stage | 说明 |
|---|---|
COLLSCAN |
全集合扫描(没有索引) |
IXSCAN |
索引扫描 |
FETCH |
获取完整文档 |
SHARD_MERGE |
合并分片结果 |
SORT |
内存排序 |
LIMIT |
限制返回数量 |
SKIP |
跳过文档 |
7.6 覆盖查询
// 覆盖查询:所有查询字段都在索引中
// 索引
db.users.createIndex({ username: 1, email: 1 })
// 覆盖查询
db.users.find(
{ username: "alice" },
{ username: 1, email: 1, _id: 0 }
)
// 验证覆盖查询
db.users.find(
{ username: "alice" },
{ username: 1, email: 1, _id: 0 }
).explain("executionStats")
// 期望看到 stage: "PROJECTION_COVERED" 或 IXSCAN + FETCH 变为 IXSCAN
7.7 索引使用策略
选择性原则
┌─────────────────────────────────────────────────────────────────┐
│ 索引选择原则 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 等值查询字段优先 → 放在复合索引前面 │
│ { status: 1, created_at: -1 } │
│ │
│ 2. 范围查询字段放后面 │
│ { status: 1, created_at: -1 } │
│ ↑ 等值 ↑ 范围 │
│ │
│ 3. 高选择性字段优先 │
│ 选择性 = 不同值数量 / 总文档数 │
│ │
│ 4. 考虑查询模式 │
│ 常见查询:{ a: 1, b: 1 } │
│ 索引:{ a: 1, b: 1, c: 1 } 可复用 │
│ │
└─────────────────────────────────────────────────────────────────┘
索引排序
// 查询排序
db.orders.find({ status: "completed" })
.sort({ created_at: -1, _id: -1 })
// 复合索引设计
db.orders.createIndex({ status: 1, created_at: -1, _id: -1 })
// ↑ 过滤 ↑ 排序1 ↑ 排序2
// 注意排序方向
// 索引 { a: 1, b: 1 } 可以支持
// { a: 1, b: 1 } ✓
// { a: 1, b: -1 } ✓
// { a: -1, b: 1 } ✓
// { a: -1, b: -1 } ✓
//
// 索引 { a: 1, b: -1 } 只能支持
// { a: 1, b: -1 } ✓
// { a: 1 } ✓
// 不能支持 { a: 1, b: 1 }
7.8 索引维护
重建索引
// 重建集合的所有索引
db.users.reIndex()
// 通常不需要手动重建,MongoDB 会自动维护
索引监控
// 查看索引使用统计
db.users.aggregate([
{ $indexStats: {} },
{
$project: {
name: 1,
accesses: "$accesses.ops",
since: "$accesses.since"
}
}
])
// 检查未使用的索引
// 如果索引的 accesses.ops 长期为 0,考虑删除
7.9 性能优化建议
常见优化模式
// ❌ 避免:字段类型不匹配
db.users.find({ _id: "507f1f77bcf86cd799439011" }) // 字符串
db.users.find({ _id: ObjectId("507f1f77bcf86cd799439011") }) // ObjectId
// ✓ 正确:使用正确的类型
db.users.find({ _id: ObjectId("507f1f77bcf86cd799439011") })
// ❌ 避免:正则不以 ^ 开头
db.users.find({ email: /example\.com/ })
// ✓ 正确:正则以 ^ 开头
db.users.find({ email: /^[\w.-]+@example\.com$/ })
// ❌ 避免:$or 连接多个独立查询
db.users.find({
$or: [
{ username: "alice" },
{ email: "alice@example.com" }
]
})
// ✓ 正确:使用 $in 替代 $or
db.users.find({
$or: [
{ username: { $in: ["alice"] } },
{ email: { $in: ["alice@example.com"] } }
]
})
// 更好的方式:创建复合索引 (username, email)
分页优化
// ❌ 避免:深度分页 skip
db.articles.find().skip(10000).limit(10)
// ✓ 正确:基于键的分页
const lastDoc = db.articles.findOne({ _id: lastId })
db.articles.find({
_id: { $gt: lastId }
}).limit(10)
// ✓ 正确:使用游标
const cursor = db.articles.find().sort({ _id: 1 }).batchSize(100)
投影优化
// ❌ 避免:返回所有字段
db.articles.find({ category: "tech" })
// ✓ 正确:只返回需要的字段
db.articles.find(
{ category: "tech" },
{ title: 1, published_at: 1, _id: 0 }
)
// ✓ 正确:排除大字段
db.articles.find(
{ category: "tech" },
{ content: 0, raw_html: 0 }
)
💡 实践提示
- 分析慢查询:使用
profiler或日志识别慢查询 - 限制索引数量:每个索引都会增加写入开销
- 监控索引使用:删除不使用的索引节省空间和开销
- 考虑覆盖索引:避免回表查询
- 合理使用复合索引:字段顺序决定索引的可用性
📚 继续学习