跳转至

第五章:索引优化

索引概述

索引是 MongoDB 提高查询性能的关键机制。没有索引,MongoDB 必须扫描整个集合来匹配查询条件。

索引类型

// 查看集合索引
db.users.getIndexes()

// 默认 _id 索引
[
    { "v": 2, "key": { "_id": 1 }, "name": "_id_" }
]

创建索引

单字段索引

// 创建升序索引
db.users.createIndex({ name: 1 })

// 创建降序索引
db.users.createIndex({ created_at: -1 })

// 后台创建(不阻塞操作)
db.users.createIndex({ email: 1 }, { background: true })

// 指定名称
db.users.createIndex({ name: 1 }, { name: "idx_name" })

复合索引

// 复合索引
db.orders.createIndex({ user_id: 1, created_at: -1 })

// 索引顺序很重要
// 支持查询:
// - user_id
// - user_id + created_at
// 不支持:
// - created_at(单独查询)

多键索引(数组索引)

// 数组字段索引
db.products.createIndex({ tags: 1 })

// 支持查询
db.products.find({ tags: "electronics" })

文本索引

// 创建文本索引
db.articles.createIndex({ title: "text", content: "text" })

// 文本搜索
db.articles.find({ $text: { $search: "MongoDB 教程" } })

// 带权重
db.articles.createIndex(
    { title: "text", content: "text" },
    { weights: { title: 10, content: 5 } }
)

// 查看相关性分数
db.articles.find(
    { $text: { $search: "MongoDB" } },
    { score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })

地理空间索引

// 2dsphere 索引
db.places.createIndex({ location: "2dsphere" })

// 附近查询
db.places.find({
    location: {
        $near: {
            $geometry: {
                type: "Point",
                coordinates: [116.4074, 39.9042]  // 北京
            },
            $maxDistance: 5000  // 5km
        }
    }
})

// 范围查询
db.places.find({
    location: {
        $geoWithin: {
            $centerSphere: [
                [116.4074, 39.9042],
                5 / 6378.1  // 5km 弧度
            ]
        }
    }
})

哈希索引

// 哈希索引(适合等值查询)
db.users.createIndex({ user_id: "hashed" })

// 分片键常用

通配符索引

// 通配符索引(MongoDB 4.2+)
db.products.createIndex({ "attributes.$**": 1 })

// 支持查询任意嵌套字段
db.products.find({ "attributes.color": "red" })

部分索引

// 只索引满足条件的文档
db.orders.createIndex(
    { status: 1, created_at: -1 },
    { partialFilterExpression: { status: "completed" } }
)

// 只索引存在的字段
db.users.createIndex(
    { email: 1 },
    { partialFilterExpression: { email: { $exists: true } } }
)

稀疏索引

// 只索引存在该字段的文档
db.users.createIndex({ nickname: 1 }, { sparse: true })

TTL 索引

// 自动过期删除
db.sessions.createIndex(
    { created_at: 1 },
    { expireAfterSeconds: 3600 }  // 1 小时后删除
)

// 指定过期时间
db.logs.createIndex(
    { expireAt: 1 },
    { expireAfterSeconds: 0 }  // 在 expireAt 时间删除
)

唯一索引

// 唯一索引
db.users.createIndex({ email: 1 }, { unique: true })

// 复合唯一索引
db.orders.createIndex({ user_id: 1, order_no: 1 }, { unique: true })

索引属性

db.collection.createIndex(
    { field: 1 },
    {
        name: "index_name",           // 索引名称
        unique: true,                  // 唯一性
        sparse: true,                  // 稀疏索引
        background: true,              // 后台创建
        expireAfterSeconds: 3600,      // TTL
        partialFilterExpression: {...}, // 部分索引条件
        weights: {...},                // 文本索引权重
        default_language: "english",   // 文本索引语言
        collation: { locale: "zh" }    // 排序规则
    }
)

索引管理

查看索引

// 查看所有索引
db.users.getIndexes()

// 查看索引大小
db.users.totalIndexSize()

// 查看索引详情
db.users.aggregate([
    { $indexStats: {} }
])

删除索引

// 删除指定索引
db.users.dropIndex("idx_name")

// 删除指定字段索引
db.users.dropIndex({ name: 1 })

// 删除所有索引(保留 _id)
db.users.dropIndexes()

重建索引

// 重建所有索引
db.users.reIndex()

查询计划分析

explain()

// 基本分析
db.users.find({ name: "张三" }).explain()

// 详细分析
db.users.find({ name: "张三" }).explain("executionStats")

// 全部分析
db.users.find({ name: "张三" }).explain("allPlansExecution")

执行计划字段

{
    "queryPlanner": {
        "plannerVersion": 1,
        "namespace": "mydb.users",
        "indexFilterSet": false,
        "parsedQuery": { "name": { "$eq": "张三" } },
        "winningPlan": {
            "stage": "FETCH",
            "inputStage": {
                "stage": "IXSCAN",      // 索引扫描
                "keyPattern": { "name": 1 },
                "indexName": "name_1"
            }
        },
        "rejectedPlans": []
    },
    "executionStats": {
        "executionSuccess": true,
        "nReturned": 1,              // 返回文档数
        "executionTimeMillis": 0,    // 执行时间
        "totalKeysExamined": 1,      // 扫描索引键数
        "totalDocsExamined": 1,      // 扫描文档数
        "indexUsed": "name_1"
    }
}

扫描类型

stage 说明
COLLSCAN 全表扫描(无索引)
IXSCAN 索引扫描
FETCH 根据索引获取文档
SORT 内存排序
SORT_KEY_GENERATOR 排序键生成

优化指标

// 理想情况
totalKeysExamined  nReturned
totalDocsExamined  nReturned

// 需要优化
totalDocsExamined >> nReturned  // 扫描太多文档

索引优化策略

ESR 规则

索引字段顺序:Equality → Sort → Range

// 查询
db.orders.find({ user_id: "u001" })
    .sort({ created_at: -1 })
    .skip(0).limit(10)

// 最优索引
db.orders.createIndex({ user_id: 1, created_at: -1 })

// E: user_id (等值)
// S: created_at (排序)
// R: 无范围查询

覆盖索引

// 创建覆盖索引
db.users.createIndex({ name: 1, email: 1 })

// 覆盖查询(不需要回表)
db.users.find(
    { name: "张三" },
    { _id: 0, name: 1, email: 1 }  // 只返回索引字段
)

// 验证
db.users.find({ name: "张三" }, { _id: 0, name: 1, email: 1 })
    .explain("executionStats")
// winningPlan.stage 应为 "PROJECTION" 而非 "FETCH"

避免内存排序

// 查询需要排序
db.orders.find({ status: "completed" }).sort({ created_at: -1 })

// 好的索引(支持排序)
db.orders.createIndex({ status: 1, created_at: -1 })

// 差的索引(需要内存排序)
db.orders.createIndex({ status: 1 })
// explain 会显示 SORT stage

索引交集

// MongoDB 可以使用多个索引
db.orders.createIndex({ user_id: 1 })
db.orders.createIndex({ status: 1 })

// 查询可能使用索引交集
db.orders.find({ user_id: "u001", status: "completed" })

// 但复合索引通常更高效
db.orders.createIndex({ user_id: 1, status: 1 })

索引使用建议

适合创建索引的场景

  1. 高频查询字段
  2. 排序字段
  3. 连接字段($lookup)
  4. 范围查询字段

避免创建索引的场景

  1. 低选择性字段(如性别、状态只有几个值)
  2. 写入频繁、查询少的集合
  3. 大数组字段(多键索引占用大)

索引数量建议

// 查看索引数量
db.users.getIndexes().length

// 建议:每个集合 3-5 个索引
// 过多索引影响写入性能

索引监控

索引使用统计

db.users.aggregate([
    { $indexStats: {} },
    {
        $project: {
            name: 1,
            accesses: 1,
            "accesses.ops": 1,
            "accesses.since": 1
        }
    }
])

未使用索引检测

// 查找访问次数为 0 的索引
db.users.aggregate([
    { $indexStats: {} },
    { $match: { "accesses.ops": 0 } },
    { $project: { name: 1 } }
])

慢查询分析

// 开启 Profiler
db.setProfilingLevel(1, { slowms: 50 })

// 查看慢查询
db.system.profile.find().sort({ ts: -1 }).limit(10)

// 分析慢查询
db.system.profile.find({ millis: { $gt: 100 } })

小结

本章学习了:

  • ✅ 索引类型(单字段、复合、文本、地理空间等)
  • ✅ 索引属性(唯一、稀疏、TTL、部分索引)
  • ✅ 查询计划分析
  • ✅ 索引优化策略(ESR 规则、覆盖索引)
  • ✅ 索引监控

下一章

第六章:Python 集成 - 学习使用 Python 操作 MongoDB。