Skip to content

索引

索引是提高MongoDB查询性能的关键。本章将介绍MongoDB索引的类型、创建方法、管理操作以及查询优化技巧。

索引概述

什么是索引

javascript
// 索引是一种数据结构,用于快速查找文档

// MongoDB索引的特点:
// 1. 类似于书的目录,可以快速定位数据
// 2. 索引存储在内存中,查询时优先使用索引
// 3. 索引会占用存储空间
// 4. 索引会影响写入性能(需要维护索引)

// 默认索引
// MongoDB在每个集合上默认创建_id字段的唯一索引

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

// 输出示例:
// [
//   {
//     v: 2,
//     key: { _id: 1 },
//     name: '_id_'
//   }
// ]

索引类型

javascript
// MongoDB支持的索引类型:

// 1. 单字段索引(Single Field Index)
//    对单个字段建立索引

// 2. 复合索引(Compound Index)
//    对多个字段建立索引

// 3. 多键索引(Multikey Index)
//    对数组字段建立索引

// 4. 文本索引(Text Index)
//    用于全文搜索

// 5. 地理空间索引(Geospatial Index)
//    用于地理位置查询

// 6. 哈希索引(Hashed Index)
//    对字段值进行哈希

// 7. 通配符索引(Wildcard Index)
//    对未知字段建立索引

创建索引

单字段索引

javascript
// 创建单字段索引

// 基本语法
db.collection.createIndex({ field: 1 })  // 1表示升序,-1表示降序

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

// 输出:'name_1'(索引名称)

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

// 指定索引名称
db.users.createIndex(
    { email: 1 },
    { name: "email_index" }
)

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

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

// 创建部分索引(只索引满足条件的文档)
db.users.createIndex(
    { status: 1 },
    { 
        partialFilterExpression: {
            age: { $gte: 18 }  // 只索引age>=18的文档
        }
    }
)

// 后台创建索引(不阻塞其他操作)
db.users.createIndex(
    { name: 1 },
    { background: true }
)

复合索引

javascript
// 创建复合索引(多个字段)

// 创建复合索引
db.users.createIndex({ city: 1, age: -1 })

// 复合索引的使用规则:
// 1. 最左前缀原则:查询条件必须包含索引的第一个字段
// 2. 索引顺序影响查询效率

// 可以使用该索引的查询:
db.users.find({ city: "北京" })                    // 使用索引
db.users.find({ city: "北京", age: { $gte: 20 } }) // 使用索引
db.users.find({ city: "北京" }).sort({ age: -1 })  // 使用索引

// 不能使用该索引的查询:
db.users.find({ age: { $gte: 20 } })  // 不包含city字段

// 创建多字段复合索引
db.orders.createIndex({ 
    userId: 1, 
    status: 1, 
    createdAt: -1 
})

// 指定索引过期时间(TTL索引)
db.logs.createIndex(
    { createdAt: 1 },
    { expireAfterSeconds: 3600 }  // 文档在创建后3600秒自动删除
)

多键索引

javascript
// 多键索引(数组字段索引)

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

// 如果字段值是数组,MongoDB自动创建多键索引

// 对数组中的嵌套字段创建索引
db.products.createIndex({ "variants.price": 1 })

// 多键索引的使用
db.products.find({ tags: "电子" })  // 使用多键索引

// 注意:复合索引中只能有一个多键索引字段

文本索引

javascript
// 文本索引用于全文搜索

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

// 创建全字段文本索引
db.articles.createIndex({ "$**": "text" })

// 指定权重
db.articles.createIndex(
    { 
        title: "text", 
        content: "text" 
    },
    { 
        weights: { 
            title: 10,     // title权重更高
            content: 5 
        },
        default_language: "none"  // 不使用语言分析器
    }
)

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

// 搜索包含任意一个词
db.articles.find({ $text: { $search: "MongoDB 教程" } })

// 搜索必须包含某个词(使用双引号)
db.articles.find({ $text: { $search: "\"MongoDB\" 教程" } })

// 排除某个词(使用减号)
db.articles.find({ $text: { $search: "MongoDB -MySQL" } })

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

// 注意:每个集合只能有一个文本索引

地理空间索引

javascript
// 地理空间索引

// 2dsphere索引(地球表面坐标)
db.places.createIndex({ location: "2dsphere" })

// 2d索引(平面坐标)
db.places.createIndex({ location: "2d" })

// 插入地理位置数据
db.places.insertOne({
    name: "北京天安门",
    location: {
        type: "Point",
        coordinates: [116.397, 39.909]  // [经度, 纬度]
    }
})

// 附近查询
db.places.find({
    location: {
        $near: {
            $geometry: {
                type: "Point",
                coordinates: [116.397, 39.909]
            },
            $maxDistance: 1000  // 最大距离(米)
        }
    }
})

// 范围查询
db.places.find({
    location: {
        $geoWithin: {
            $centerSphere: [
                [116.397, 39.909],  // 圆心
                10 / 6378.1         // 半径(弧度)
            ]
        }
    }
})

哈希索引

javascript
// 哈希索引

// 创建哈希索引
db.users.createIndex({ userId: "hashed" })

// 哈希索引特点:
// 1. 支持等值查询,不支持范围查询
// 2. 分布均匀,适合分片键
// 3. 不能用于排序

// 使用哈希索引查询
db.users.find({ userId: 12345 })  // 等值查询可以使用

// 不能使用哈希索引的查询
db.users.find({ userId: { $gt: 10000 } })  // 范围查询不能使用

管理索引

查看索引

javascript
// 查看集合的所有索引
db.users.getIndexes()

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

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

// 查看特定索引的使用情况
db.users.aggregate([
    { $indexStats: {} },
    { $match: { name: "name_1" } }
])

删除索引

javascript
// 删除索引

// 按名称删除索引
db.users.dropIndex("name_1")

// 按规范删除索引
db.users.dropIndex({ name: 1 })

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

// 删除多个指定索引
db.users.dropIndexes(["name_1", "age_-1"])

修改索引

javascript
// MongoDB不支持直接修改索引
// 需要先删除再重新创建

// 修改索引步骤:
// 1. 删除旧索引
db.users.dropIndex("old_index")

// 2. 创建新索引
db.users.createIndex({ name: 1 }, { name: "new_index" })

// 隐藏索引(MongoDB 4.4+)
// 隐藏索引不会被查询使用,但会继续维护
db.users.hideIndex("name_1")

// 取消隐藏
db.users.unhideIndex("name_1")

执行计划分析

explain方法

javascript
// 使用explain分析查询执行计划

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

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

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

// 输出关键字段说明:
// - winningPlan: 被选中的执行计划
// - stage: 执行阶段(COLLSCAN全表扫描,IXSCAN索引扫描)
// - indexName: 使用的索引名称
// - executionTimeMillis: 执行时间(毫秒)
// - totalDocsExamined: 扫描的文档数
// - nReturned: 返回的文档数

// 判断是否使用索引
var plan = db.users.find({ name: "张三" }).explain()
if (plan.queryPlanner.winningPlan.stage === "COLLSCAN") {
    print("全表扫描,未使用索引")
} else if (plan.queryPlanner.winningPlan.stage === "IXSCAN") {
    print("使用了索引: " + plan.queryPlanner.winningPlan.indexName)
}

优化查询

javascript
// 查询优化技巧

// 1. 使用hint强制使用指定索引
db.users.find({ name: "张三" }).hint({ name: 1 })

// 2. 检查扫描文档数与返回文档数的比例
var stats = db.users.find({ name: "张三" }).explain("executionStats")
var examined = stats.executionStats.totalDocsExamined
var returned = stats.executionStats.nReturned
print("扫描文档数: " + examined)
print("返回文档数: " + returned)
print("比例: " + (examined / returned))
// 理想情况是1:1,比例过大说明需要优化

// 3. 使用覆盖索引
// 覆盖索引:查询的所有字段都在索引中,不需要回表
db.users.createIndex({ name: 1, email: 1 })

db.users.find(
    { name: "张三" },
    { _id: 0, name: 1, email: 1 }  // 只返回索引字段
).explain("executionStats")
// 查看executionStats.totalDocsExamined是否为0

// 4. 避免使用$where和$regex开头通配符
// 这些操作无法使用索引

// 5. 使用projection减少返回数据量
db.users.find({ name: "张三" }, { name: 1, email: 1, _id: 0 })

索引设计原则

javascript
// 索引设计原则

// 1. ESR原则(Equality-Sort-Range)
//    等值查询字段 -> 排序字段 -> 范围查询字段

// 示例:查询城市为北京的用户,按年龄排序,筛选薪资范围
db.users.find({ city: "北京", salary: { $gte: 5000 } }).sort({ age: 1 })

// 推荐索引:
db.users.createIndex({ city: 1, age: 1, salary: 1 })
// city: 等值查询(Equality)
// age: 排序字段(Sort)
// salary: 范围查询(Range)

// 2. 选择性高的字段优先
// 选择性 = 不同值数量 / 总文档数
// 选择性越高,索引效果越好

// 3. 避免过多索引
// - 索引占用存储空间
// - 影响写入性能
// - 建议单集合索引不超过5-10个

// 4. 使用复合索引代替多个单字段索引

// 5. 定期检查索引使用情况
db.users.aggregate([
    { $indexStats: {} },
    { $match: { accesses: { $eq: 0 } } }  // 查找未使用的索引
])

本章小结

本章介绍了MongoDB索引的相关知识:

  1. 索引概述:理解索引的概念和类型
  2. 创建索引:掌握单字段、复合、多键、文本、地理空间、哈希索引的创建
  3. 管理索引:学会查看、删除、修改索引
  4. 执行计划:学会使用explain分析查询性能
  5. 查询优化:掌握覆盖索引、hint等优化技巧
  6. 设计原则:了解ESR原则和索引设计最佳实践

下一章,我们将学习聚合框架,了解MongoDB强大的数据处理能力。