Appearance
索引优化
索引概述
索引原理
MongoDB 使用 B-Tree 数据结构存储索引:
[根节点]
/ | \
[非叶节点] [非叶节点] [非叶节点]
/ | \
[叶子节点] [叶子节点] [叶子节点]
↓ ↓ ↓
[文档指针] [文档指针] [文档指针]索引类型
| 类型 | 说明 |
|---|---|
| 单字段索引 | 单个字段的索引 |
| 复合索引 | 多个字段的组合索引 |
| 多键索引 | 数组字段的索引 |
| 文本索引 | 全文搜索索引 |
| 地理空间索引 | 地理位置索引 |
| 哈希索引 | 哈希值索引 |
| 通配符索引 | 动态字段索引 |
创建索引
单字段索引
javascript
db.users.createIndex({ name: 1 })
db.users.createIndex({ age: -1 })复合索引
javascript
db.users.createIndex({ name: 1, age: -1 })
db.users.createIndex({ city: 1, age: -1, name: 1 })多键索引
javascript
db.users.createIndex({ tags: 1 })
db.users.createIndex({ 'addresses.city': 1 })文本索引
javascript
db.articles.createIndex({ title: 'text', content: 'text' })
db.articles.createIndex({ '$**': 'text' })地理空间索引
javascript
db.places.createIndex({ location: '2dsphere' })
db.places.createIndex({ location: '2d' })哈希索引
javascript
db.users.createIndex({ _id: 'hashed' })通配符索引
javascript
db.users.createIndex({ 'profile.$**': 1 })索引选项
唯一索引
javascript
db.users.createIndex({ email: 1 }, { unique: true })稀疏索引
javascript
db.users.createIndex({ phone: 1 }, { sparse: true })TTL 索引
javascript
db.logs.createIndex({ created_at: 1 }, { expireAfterSeconds: 3600 })部分索引
javascript
db.users.createIndex(
{ name: 1 },
{ partialFilterExpression: { age: { $gte: 18 } } }
)后台创建
javascript
db.users.createIndex({ name: 1 }, { background: true })索引名称
javascript
db.users.createIndex({ name: 1 }, { name: 'idx_user_name' })排序规则
javascript
db.users.createIndex(
{ name: 1 },
{ collation: { locale: 'zh', strength: 2 } }
)完整选项示例
javascript
db.users.createIndex(
{ name: 1, age: -1 },
{
name: 'idx_name_age',
unique: false,
sparse: true,
background: true,
partialFilterExpression: { status: 'active' },
collation: { locale: 'zh' }
}
)索引管理
查看索引
javascript
db.users.getIndexes()查看索引大小
javascript
db.users.totalIndexSize()删除索引
javascript
db.users.dropIndex('name_1')
db.users.dropIndex({ name: 1 })
db.users.dropIndexes()
db.users.dropIndexes(['name_1', 'age_-1'])重建索引
javascript
db.users.reIndex()索引设计原则
1. ESR 原则
索引字段顺序:Equality → Sort → Range
javascript
db.users.find({ city: '北京' }).sort({ age: -1 }).limit(10)
db.users.createIndex({ city: 1, age: -1 })2. 选择性高的字段
javascript
db.users.aggregate([
{
$group: {
_id: '$status',
count: { $sum: 1 }
}
}
])选择性越高,索引效果越好。
3. 覆盖索引
javascript
db.users.createIndex({ name: 1, email: 1 })
db.users.find(
{ name: '张三' },
{ _id: 0, name: 1, email: 1 }
)4. 避免索引失效
javascript
db.users.find({ name: /^张/ })
db.users.find({ name: { $regex: '^张' } })
db.users.find({ age: { $ne: 25 } })
db.users.find({ age: { $nin: [20, 25, 30] } })
db.users.find({ $or: [{ name: '张三' }, { age: 25 }] })5. 复合索引最左前缀
javascript
db.users.createIndex({ city: 1, age: -1, name: 1 })
db.users.find({ city: '北京' })
db.users.find({ city: '北京', age: { $gt: 20 } })
db.users.find({ city: '北京', age: { $gt: 20 }, name: '张三' })
db.users.find({ age: 25 })
db.users.find({ age: 25, name: '张三' })执行计划分析
explain
javascript
db.users.find({ name: '张三' }).explain()
db.users.find({ name: '张三' }).explain('executionStats')
db.users.find({ name: '张三' }).explain('allPlansExecution')关键字段
| 字段 | 说明 |
|---|---|
| winningPlan | 选中的执行计划 |
| executionStats | 执行统计信息 |
| totalDocsExamined | 扫描文档数 |
| totalKeysExamined | 扫描索引键数 |
| nReturned | 返回文档数 |
| executionTimeMillis | 执行时间 |
执行阶段
| 阶段 | 说明 |
|---|---|
| COLLSCAN | 全表扫描 |
| IXSCAN | 索引扫描 |
| FETCH | 获取文档 |
| SORT | 内存排序 |
| SORT_KEY_GENERATOR | 排序键生成 |
| LIMIT | 限制结果 |
优化目标
totalDocsExamined ≈ nReturned
totalKeysExamined ≈ nReturned分析示例
javascript
db.users.find({ name: '张三' }).explain('executionStats')
{
"executionStats": {
"totalDocsExamined": 1,
"totalKeysExamined": 1,
"nReturned": 1,
"executionTimeMillis": 0
},
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"indexName": "name_1"
}
}
}索引监控
查看索引使用情况
javascript
db.users.aggregate([
{ $indexStats: {} }
])查看未使用的索引
javascript
db.users.aggregate([
{ $indexStats: {} },
{ $match: { 'accesses.ops': 0 } }
])索引统计信息
| 字段 | 说明 |
|---|---|
| name | 索引名称 |
| accesses.ops | 访问次数 |
| accesses.since | 统计开始时间 |
索引优化案例
案例1:优化排序
javascript
db.users.find({ city: '北京' }).sort({ age: -1 })
db.users.createIndex({ city: 1, age: -1 })案例2:优化分页
javascript
db.users.find().skip(10000).limit(10)
let lastId = ObjectId('...')
db.users.find({ _id: { $gt: lastId } }).limit(10)案例3:优化 $or 查询
javascript
db.users.find({
$or: [
{ name: '张三' },
{ email: 'test@example.com' }
]
})
db.users.createIndex({ name: 1 })
db.users.createIndex({ email: 1 })案例4:优化数组查询
javascript
db.users.find({ tags: 'tech' })
db.users.createIndex({ tags: 1 })案例5:优化嵌套文档查询
javascript
db.users.find({ 'profile.age': 25 })
db.users.createIndex({ 'profile.age': 1 })特殊索引
TTL 索引
自动删除过期文档:
javascript
db.logs.createIndex(
{ created_at: 1 },
{ expireAfterSeconds: 3600 }
)
db.sessions.createIndex(
{ lastAccess: 1 },
{ expireAfterSeconds: 1800 }
)部分索引
只索引符合条件的文档:
javascript
db.orders.createIndex(
{ order_date: 1 },
{ partialFilterExpression: { status: 'completed' } }
)稀疏索引
只索引存在该字段的文档:
javascript
db.users.createIndex(
{ phone: 1 },
{ sparse: true }
)大小写不敏感索引
javascript
db.users.createIndex(
{ name: 1 },
{ collation: { locale: 'zh', strength: 2 } }
)
db.users.find({ name: 'zhang' }).collation({ locale: 'zh', strength: 2 })索引限制
索引数量限制
- 单个集合最多 64 个索引
- 复合索引最多 32 个字段
索引键长度限制
- 单个索引键最大 1024 字节
内存限制
- 索引必须能放入内存
检查索引大小
javascript
db.users.stats().indexSizes索引构建策略
前台构建 vs 后台构建
| 方式 | 特点 |
|---|---|
| 前台 | 快速,阻塞写操作 |
| 后台 | 较慢,不阻塞写操作 |
javascript
db.users.createIndex({ name: 1 })
db.users.createIndex({ name: 1 }, { background: true })在副本集上构建索引
javascript
db.adminCommand({
setParameter: 1,
disableJavaScript: true
})
rs.stepDown()滚动构建索引
- 停止一个从节点
- 单机模式启动
- 构建索引
- 重启为从节点
- 重复以上步骤