认识
一、认识
Index
索引 索引能够减少 MongoDB
扫描的文档数量, 从而加速查询, 可以减少磁盘读取量, 降低 I/O
负担; 索引也可以优化排序, 可以加快 sort
操作,不需要额外的内存排序(避免 sort exceeded memory limit
错误); 唯一索引可用于确保特定字段的唯一性。 如果没有索引,MongoDB
在查询时需要遍历整个集合(Collection
)中的所有文档(Document
),这种方式称为 全表扫描(COLLSCAN
),效率低下。而有了索引后,查询时可以直接定位到目标数据,提高查找速度。MongoDB
默认会为 _id
字段自动创建索引,因此查询 _id
时效率较高。索引支持在 MongoDB
中高效执行查询。
Index
索引原理: MongoDB
使用 存储引擎(默认 WiredTiger
) 来管理索引。每个索引由 MongoDB
以 B-Tree
结构存储在磁盘上,同时部分索引也会缓存到内存中。 B-Tree
(Balance Tree
,平衡树) 是一种自平衡的多路搜索树,用于存储和高效检索大量数据。它是数据库索引的常见实现,如 MongoDB
、MySQL``、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
, 索引值存储了文档 _id
,MongoDB
通过 _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
: 全表扫描, 说明索引未生效,需要优化。