Skip to content

索引优化

索引概述

索引原理

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()

滚动构建索引

  1. 停止一个从节点
  2. 单机模式启动
  3. 构建索引
  4. 重启为从节点
  5. 重复以上步骤

下一步学习