跳到主要内容

认识

2025年02月28日
柏拉文
越努力,越幸运

一、认识


Index 索引 索引能够减少 MongoDB 扫描的文档数量, 从而加速查询, 可以减少磁盘读取量, 降低 I/O 负担; 索引也可以优化排序, 可以加快 sort 操作,不需要额外的内存排序(避免 sort exceeded memory limit 错误); 唯一索引可用于确保特定字段的唯一性。 如果没有索引,MongoDB 在查询时需要遍历整个集合(Collection)中的所有文档(Document),这种方式称为 全表扫描(COLLSCAN,效率低下。而有了索引后,查询时可以直接定位到目标数据,提高查找速度。MongoDB 默认会为 _id 字段自动创建索引,因此查询 _id 时效率较高。索引支持在 MongoDB 中高效执行查询。

Index 索引原理: MongoDB 使用 存储引擎(默认 WiredTiger 来管理索引。每个索引由 MongoDBB-Tree 结构存储在磁盘上,同时部分索引也会缓存到内存中。 B-TreeBalance Tree,平衡树) 是一种自平衡的多路搜索树,用于存储和高效检索大量数据。它是数据库索引的常见实现,如 MongoDBMySQL``、PostgreSQL 都采用了 B-Tree 或其变种(如 B+Tree)。B-Tree 是一种多路搜索树,每个节点可以包含多个键, 并有多个子节点, 一般是 16-1000 个子节点, 这大幅减少了磁盘访问次数, 相比二叉搜索树(BST)高度较高,需要更多的磁盘 I/O 访问,B-Tree 适合存储大规模索引; B-Tree 所有叶子节点的深度相同, 保证树的高度尽可能小,提高查询效率, 树的高度较低,即使百万级数据,查询通常只需 2~4 次磁盘访问, 显然,B-Tree 在数据库中的查询效率远高于二叉树; B-Tree 有序存储数据, 支持高效的范围查询, 支持排序查询(索引本身是有序的,无需额外排序), 相比哈希索引(Hash Index)更灵活(哈希索引仅支持等值查询,不支持范围查询); B-Tree 在插入和删除数据后会自动保持平衡, 保证查询性能稳定, 而普通二叉搜索树(BST)可能会退化成链表, 导致查询效率变差(O(N)), 这保证了 MongoDB 在写入数据时不会导致索引性能下降。在 MongoDB 中,每个索引节点存储一个键值(key)和对应的文档位置(value),B-Tree 结构允许 MongoDB 快速找到特定的键,并高效地进行范围查询。假设我们在 users 集合中创建了索引。 扩展: B-Tree 的自平衡机制: B-Tree 需要在插入和删除时都保持平衡,确保树的高度不会增长过快或变得不均衡。1. 插入时的自平衡: B-Tree 采用 节点分裂(Split 方式进行平衡, 当某个节点已满(达到 2t-1 个键),它会拆分为两个节点,并将中间键上移到父节点, 如果根节点被拆分,则会创建一个新的根,树的高度增加。2. 删除时的自平衡: 删除操作比插入复杂得多,可能涉及: 简单删除, 如果删除的键在叶子节点中,并且该节点仍满足 B-Tree 的最低度数(t-1 个键),直接删除即可; 借键(Borrow:如果删除的节点少于 t-1 个键,可以向相邻的兄弟节点借键,以维持 B-Tree 规则; 合并(Merge, 如果兄弟节点也不能借键,则合并两个兄弟节点,减少树的高度。

db.users.createIndex({ age: 1 }) // 针对 `age` 字段创建升序索引

// MongoDB 可能会构造如下的 B-Tree:

[30]
/ \
[20] [40, 50]
/ \ / \
[10] [25] [35] [45, 55]


// 查询 age = 35 时,MongoDB 先访问根节点 [30],然后进入右子树 [40, 50],再进入 [35],最终找到目标数据。时间复杂度: O(log N),比全表扫描(O(N))快得多。

Index 索引工作: 当我们创建索引后,MongoDB 查询时会进行以下步骤: 1. 索引遍历, MongoDB 使用 B-Tree 快速找到满足查询条件的键值; 2. 获取文档 _id, 索引值存储了文档 _idMongoDB 通过 _id 获取完整文档; 3. 返回查询结果, MongoDB 直接返回匹配的文档。

Index 索引的成本、影响、平衡: 索引能够减少 MongoDB 扫描的文档数量, 从而加速查询, 可以减少磁盘读取量, 降低 I/O 负担; 索引也可以优化排序, 可以加快 sort 操作,不需要额外的内存排序(避免 sort exceeded memory limit 错误); 唯一索引可用于确保特定字段的唯一性; 但是, 索引会占用额外的磁盘和内存, 插入、更新和删除时,索引需要同步更新,可能会降低写入性能, 另外, 不必要的索引会增加数据库的维护成本; 因此, 我们只创建必要的索引, 避免不必要的索引占用存储, 定期分析索引使用情况, 删除未使用的索引。

db.collection.stats().indexSizes // 查看索引大小
db.collection.getIndexes() // 获取所有索引

db.collection.dropIndex("age_1") // 删除 `age` 索引

Index 索引 有如下几种:

  • 单字段索引 Single Field Index: 对单个字段进行添加索引, 从而加速查询速度、sort 排序速度。如果升序或降序索引位于单个字段上,则该字段上的排序操作可以是任一方向。语法如下:

    db.accountsWithIndex.insertMany([ { name: "alice", balance: 50, currency: [ "GBP", "USD" ] }, { name: "bob", balance: 20, currency: [ "AUD", "USD" ] }, { name: "bob", balance: 300, currency: [ "CNY" ] } ]);

    db.accountsWithIndex.createIndex( { name: 1} );

    db.accountsWithIndex.getIndexes();

    // 输出结果:

    [
    { v: 2, key: { _id: 1 }, name: '_id_' },
    { v: 2, key: { name: 1 }, name: 'name_1' }
    ]

    db.accountsWithIndex.find({ name: "bob" }).explain("executionStats");

    // 输出结果:
    {
    queryPlanner: {
    winningPlan: {
    stage: 'FETCH',
    inputStage: {
    stage: 'IXSCAN',
    indexName: 'name_1',
    }
    },
    },
    }
  • 复合索引 Compound Index: 如果 keys 文档指定了超过一个字段,则 createIndex() 将创建一个复合索引。复合索引遵循前缀子集原则, 如果查询键包含索引键或索引前缀, MongoDB 会使用索引加速这些查询键的查询速度。如果排序键包含索引键或索引前缀, 且排序键的排列顺序与其在索引中出现的顺序相同, 且排序模式相同, 则 MongoDB 可以使用索引对查询结果排序。

    db.collection.createIndex({ A: 1, B: 1, C: -1  }) // A 字段升序, B 字段升序, C 字段降序 

    db.collection.find({ A: xx }).explain("executionStats") ; // A 是索引的前缀,完全利用索引 可以用 .explain("executionStats") 检查是否使用了索引。
    db.collection.find({ A: xx, B: yy}).explain("executionStats") ; // A, B 都是索引前缀,完全利用索引 可以用 .explain("executionStats") 检查是否使用了索引。
    db.collection.find({ A: xx, B: yy, C: zz }).explain("executionStats") ; // A, B, C 都是索引的一部分,完全利用索引 可以用 .explain("executionStats") 检查是否使用了索引。

    db.collection.find({ B: yy }).explain("executionStats") ; // B 不是索引的前缀,无法利用索引 可以用 .explain("executionStats") 检查是否使用了索引。
    db.collection.find({ C: zz }).explain("executionStats") ; // C 不是索引的前缀,无法利用索引 可以用 .explain("executionStats") 检查是否使用了索引。
    db.collection.find({ A: xx, C: zz }).explain("executionStats") ; // A 用索引,C 需要内存过滤,查询性能下降 可以用 .explain("executionStats") 检查是否使用了索引。
    db.collection.find({ B: yy, C: zz }).explain("executionStats") ; // B, C 都不是索引前缀,无法利用索引 可以用 .explain("executionStats") 检查是否使用了索引。B, C 都不是索引前缀,无法利用索引
  • 多键索引 Multikey Index: 用于索引数组字段,每个数组元素都会被索引。数组字段中的每一个元素, 都会在多键索引中创建一个键。

    db.collection.insert({ name: "多键索引", tags: ["MongoDB", "Database"]});
    db.collection.createIndex({ tags: 1 })

    db.collection.find({ tags: "MongoDB" }) // 适用于数组字段查询

    db.collection.find({ tags: "MongoDB" }).sort({ age: 1 }); // 不支持多键索引的排序, 可能会报错
  • 唯一索引 Unique Index: 确保字段值唯一,适用于用户名、邮箱等唯一字段。

    db.collection.insert({ name: "唯一索引", email: "unique-index@163.com"});

    db.collection.createIndex({ email: 1 }, { unique: true });

    如果尝试插入相同 email 的数据,MongoDB 会报错
  • 局部索引 Partial Index: 只索引满足特定条件的文档,以减少索引大小。适用于索引稀疏数据, 例如,某些文档可能没有 status 字段,减少不必要的索引存储。

    db.collection.createIndex({ status: 1 }, { partialFilterExpression: { status: { $exists: true } } })
  • 稀疏索引 Sparse Index: 只索引存在指定字段的文档,未索引的文档不会被包含在索引中。

    db.collection.createIndex({ phone: 1 }, { sparse: true })
  • TTL 索引 Time-To-Live Index: 自动删除超过指定时间的数据(适用于日志、缓存)。

    db.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 }) // 1小时后自动删除
  • 文字索引 Text Index: 用于全文搜索。

  • 地理空间索引 Geospatial Index: 用于存储和查询地理位置数据。

二、语法


假设有一个集合:

db.users.insertMany([
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charlie", age: 35 }
])

无索引查询时

db.users.find({ age: 30 }).explain("executionStats")

// 返回

{
"executionStats": {
"totalKeysExamined": 0,
"totalDocsExamined": 3, // 这里 totalDocsExamined 是 3,说明 MongoDB 遍历了所有文档。
"executionStages": {
"stage": "COLLSCAN" // 全表扫描
}
}
}

创建索引并查询

db.users.createIndex({ age: 1 })
db.users.find({ age: 30 }).explain("executionStats")

{
"executionStats": {
"totalKeysExamined": 1,
"totalDocsExamined": 1, // 这里 totalDocsExamined 变为 1,索引加速了查询。
"executionStages": {
"stage": "IXSCAN" // 使用了索引
}
}
}

三、操作


3.1 创建索引

db.collection.createIndex()

3.2 查看索引

db.collection.getIndexes()

3.3 删除索引

删除指定索引

db.collection.dropIndex("name_1")

删除所有索引

db.collection.dropIndexes()

3.4 分析索引

MongoDB 提供 explain() 方法来查看查询计划,检查索引是否生效。

db.collection.find({ name: "Tom" }).explain("executionStats")

// 返回
{
"queryPlanner": {
"winningPlan": {
"stage": "IXSCAN", // 说明使用了索引扫描
"keyPattern": { "name": 1 }
}
},
"executionStats": {
"totalKeysExamined": 1,
"totalDocsExamined": 1
}
}

queryPlanner.winningPlan.stage 表示当前扫描类型。类型如下:

  • IXACAN: 索引扫描, 说明索引生效。

  • COLLSCAN: 全表扫描, 说明索引未生效,需要优化。