外观
MongoDB 索引原理
索引的作用
- 加速查询:减少查询时需要扫描的文档数量
- 支持排序:加速排序操作
- 强制唯一性:通过唯一索引确保字段值的唯一性
- 支持范围查询:提高范围查询的性能
- 优化聚合操作:加速聚合管道中的匹配和排序阶段
索引数据结构
B树索引
MongoDB使用B树作为默认的索引数据结构,B树具有以下特点:
- 平衡结构:B树是自平衡的,所有叶子节点位于同一层
- 多路搜索:每个节点可以有多个子节点,减少树的高度
- 有序存储:索引键按顺序存储,支持范围查询和排序
- 高效插入删除:插入和删除操作的时间复杂度为O(log n)
- 磁盘友好:节点大小设计为磁盘块大小,减少磁盘I/O
B树与B+树的区别
MongoDB使用的是B树而不是B+树,两者的主要区别:
| 特性 | B树 | B+树 |
|---|---|---|
| 数据存储 | 所有节点都存储数据 | 只有叶子节点存储数据 |
| 叶子节点连接 | 没有 | 有(双向链表) |
| 范围查询 | 需要回溯 | 高效(通过叶子节点链表) |
| 适合场景 | 随机访问 | 范围查询 |
索引结构示例
[ root ]
/ \
[ page 1 ] [ page 2 ]
/ | \ / | \
[ leaf 1 ] [ leaf 2 ] [ leaf 3 ] [ leaf 4 ]每个节点包含索引键和指向子节点或文档的指针:
- 根节点:指向中间页或叶子页
- 中间页:指向其他中间页或叶子页
- 叶子页:存储索引键和指向文档的指针(或文档本身,对于覆盖索引)
索引类型
单字段索引
单字段索引是最基本的索引类型,基于单个字段创建索引:
javascript
// 创建单字段索引
db.collection.createIndex({ field: 1 }) // 升序索引
db.collection.createIndex({ field: -1 }) // 降序索引复合索引
复合索引基于多个字段创建索引,字段顺序对索引性能有重要影响:
javascript
// 创建复合索引
db.collection.createIndex({ field1: 1, field2: -1 })复合索引的前缀原则
复合索引支持前缀查询,例如索引 { a: 1, b: 1, c: 1 } 可以支持:
{ a: ... }{ a: ..., b: ... }{ a: ..., b: ..., c: ... }
但不支持:
{ b: ... }{ b: ..., c: ... }{ c: ... }
多键索引
当字段值是数组时,MongoDB会自动创建多键索引,为数组中的每个元素创建索引条目:
javascript
// 文档结构
{ _id: 1, tags: [ "mongodb", "database", "nosql" ] }
// 创建多键索引
db.collection.createIndex({ tags: 1 })地理空间索引
用于地理位置数据的查询,支持点、线、面等地理空间数据类型:
javascript
// 2dsphere索引(用于地球表面的地理空间数据)
db.collection.createIndex({ location: "2dsphere" })
// 2d索引(用于平面地理空间数据)
db.collection.createIndex({ location: "2d" })文本索引
用于全文搜索,支持对字符串内容进行分词和搜索:
javascript
// 创建文本索引
db.collection.createIndex({ content: "text" })
// 多字段文本索引
db.collection.createIndex({ title: "text", content: "text" })哈希索引
基于字段的哈希值创建索引,用于相等查询,不支持范围查询:
javascript
// 创建哈希索引
db.collection.createIndex({ field: "hashed" })唯一索引
确保索引字段的值是唯一的:
javascript
// 创建唯一索引
db.collection.createIndex({ field: 1 }, { unique: true })
// 复合唯一索引
db.collection.createIndex({ field1: 1, field2: 1 }, { unique: true })稀疏索引
只包含具有索引字段的文档,跳过没有索引字段的文档:
javascript
// 创建稀疏索引
db.collection.createIndex({ field: 1 }, { sparse: true })部分索引
基于过滤条件创建索引,只包含满足条件的文档:
javascript
// 创建部分索引
db.collection.createIndex(
{ field: 1 },
{ partialFilterExpression: { status: { $eq: "active" } } }
)索引工作原理
查询优化器
MongoDB查询优化器会评估多个查询计划,选择成本最低的计划执行:
- 生成查询计划:针对每个可能的索引生成查询计划
- 评估计划成本:基于文档扫描数量、索引使用情况等评估成本
- 选择最佳计划:选择成本最低的计划执行
- 缓存计划:缓存查询计划,提高重复查询的性能
索引扫描类型
- IXSCAN:索引扫描,直接使用索引查找文档
- COLLSCAN:全表扫描,扫描整个集合查找文档
- FETCH:获取文档,从磁盘读取文档
- SORT:排序,对结果进行排序
- LIMIT:限制结果数量
索引使用示例
javascript
// 创建索引
db.users.createIndex({ age: 1, name: 1 })
// 查询1:使用索引(前缀匹配)
db.users.find({ age: { $gt: 25 } })
// 查询2:使用索引(复合匹配)
db.users.find({ age: 30, name: { $regex: /^A/ } })
// 查询3:使用索引排序
db.users.find().sort({ age: 1, name: 1 })
// 查询4:不使用索引(不符合前缀原则)
db.users.find({ name: "Alice" })索引创建与管理
创建索引
javascript
// 基本语法
db.collection.createIndex(
{ field1: 1, field2: -1 }, // 索引键和方向
{ // 索引选项
name: "custom_index_name", // 索引名称
unique: false, // 是否唯一
sparse: false, // 是否稀疏
expireAfterSeconds: 3600, // TTL索引(秒)
collation: { locale: "en" } // 排序规则
}
)查看索引
javascript
// 查看集合的所有索引
db.collection.getIndexes()
// 查看索引大小
db.collection.totalIndexSize()删除索引
javascript
// 删除指定索引
db.collection.dropIndex("index_name")
// 删除所有索引(保留_id索引)
db.collection.dropIndexes()重建索引
javascript
// 重建所有索引
db.collection.reIndex()
// 重建指定索引
db.collection.dropIndex("index_name")
db.collection.createIndex({ field: 1 }, { name: "index_name" })索引性能优化
索引选择性
索引选择性是指索引字段的唯一值数量与集合中文档数量的比值,选择性越高,索引效果越好:
javascript
// 高选择性索引(唯一值多)
db.users.createIndex({ email: 1 })
// 低选择性索引(唯一值少)
db.users.createIndex({ gender: 1 })覆盖索引
覆盖索引包含查询所需的所有字段,不需要额外的文档获取操作:
javascript
// 查询:db.users.find({ age: 30 }, { name: 1, email: 1, _id: 0 })
// 创建覆盖索引
db.users.createIndex({ age: 1, name: 1, email: 1 })索引交集
MongoDB支持使用多个索引的交集来满足查询:
javascript
// 创建两个索引
db.collection.createIndex({ field1: 1 })
db.collection.createIndex({ field2: 1 })
// 查询使用索引交集
db.collection.find({ field1: "value1", field2: "value2" })索引排序顺序
索引的排序顺序(升序/降序)会影响查询的性能,特别是对于复合索引:
javascript
// 查询排序顺序与索引一致
db.collection.find().sort({ field1: 1, field2: 1 })
// 对应的索引
db.collection.createIndex({ field1: 1, field2: 1 })
// 查询排序顺序与索引相反
db.collection.find().sort({ field1: -1, field2: -1 })
// 对应的索引(可以重用上面的索引)索引监控与分析
解释计划
使用explain()方法分析查询计划:
javascript
// 查看查询计划
db.collection.find({ field: "value" }).explain()
// 详细模式
db.collection.find({ field: "value" }).explain("executionStats")
// 全量模式
db.collection.find({ field: "value" }).explain("allPlansExecution")索引使用统计
查看索引的使用情况:
javascript
// 启用索引使用统计
db.setProfilingLevel(1, { slowms: 100 })
// 查看索引使用统计
db.system.profile.find({
op: "query",
"executionStats.executionStages.inputStage.stage": "IXSCAN"
}).limit(10)索引扫描统计
javascript
// 查看索引扫描数量
db.serverStatus().indexCounters
// 查看集合级别的索引统计
db.collection.stats().indexDetails索引最佳实践
- 为常用查询创建索引:分析查询模式,为频繁使用的查询创建索引
- 遵循前缀原则:合理设计复合索引的字段顺序
- 使用覆盖索引:减少磁盘I/O,提高查询性能
- 避免过度索引:每个索引会增加写操作的开销
- 定期监控索引使用:删除不使用或低效率的索引
- 使用部分索引:只对常用文档创建索引
- 考虑索引选择性:优先为选择性高的字段创建索引
- 使用TTL索引管理过期数据:自动删除过期文档
- 在低峰期创建索引:避免影响生产环境性能
- 测试索引性能:使用explain()分析索引的效果
索引限制
- 索引键大小限制:索引键的大小不能超过1024字节
- 索引数量限制:每个集合的索引数量建议不超过64个
- 复合索引字段限制:复合索引最多可以包含32个字段
- 内存限制:索引需要加载到内存中,占用内存资源
- 写操作开销:每次写操作需要更新所有相关索引
- 索引碎片:频繁的插入删除会导致索引碎片,影响性能
常见问题(FAQ)
Q1: 什么时候需要创建索引?
A1: 当查询性能不佳,或者查询计划显示全表扫描(COLLSCAN)时,应该考虑创建索引。特别是对于频繁执行的查询、范围查询、排序操作和聚合操作,索引可以显著提高性能。
Q2: 如何选择索引的字段顺序?
A2: 索引的字段顺序应该根据查询模式确定:
- 首先放置选择性高的字段
- 其次放置常用于精确匹配的字段
- 最后放置用于范围查询或排序的字段
例如,对于查询 db.users.find({ age: 30, city: "Beijing" }).sort({ name: 1 }),最佳索引顺序是 { age: 1, city: 1, name: 1 }。
Q3: 复合索引和多个单字段索引哪个更好?
A3: 这取决于查询模式:
- 如果查询经常同时使用多个字段,复合索引更好
- 如果查询经常只使用单个字段,多个单字段索引可能更好
- MongoDB支持索引交集,可以组合使用多个单字段索引
Q4: 如何检测不使用的索引?
A4: 可以通过以下方法检测不使用的索引:
- 使用数据库分析器(profiler)监控索引使用情况
- 查看索引访问统计信息
- 定期运行查询计划分析
- 使用第三方工具(如MongoDB Compass、Ops Manager)分析索引使用情况
Q5: 索引会影响写操作性能吗?
A5: 是的,索引会增加写操作的开销。每次插入、更新或删除文档时,MongoDB需要更新所有相关的索引。因此,应该避免过度索引,只创建必要的索引。
Q6: 如何优化索引碎片?
A6: 优化索引碎片的方法:
- 定期重建索引(db.collection.reIndex())
- 使用compact命令压缩集合
- 对于频繁更新的集合,考虑使用更大的索引页大小
- 避免频繁的插入删除操作
Q7: 唯一索引和稀疏索引可以一起使用吗?
A7: 是的,唯一索引可以和稀疏索引一起使用:
javascript
db.collection.createIndex({ field: 1 }, { unique: true, sparse: true })这会创建一个唯一索引,只包含具有该字段的文档,并且确保这些文档的字段值是唯一的。
Q8: TTL索引是如何工作的?
A8: TTL(Time To Live)索引自动删除过期文档:
- 基于日期类型字段创建
- 设置expireAfterSeconds参数指定过期时间
- MongoDB后台线程定期检查并删除过期文档
- 只能用于单字段索引
- 不支持复合索引
Q9: 如何为数组字段创建索引?
A9: MongoDB会自动为数组字段创建多键索引:
javascript
db.collection.createIndex({ arrayField: 1 })对于嵌套数组,可以使用点表示法创建索引:
javascript
db.collection.createIndex({ "arrayField.nestedField": 1 })Q10: 索引键大小超过限制怎么办?
A10: 如果索引键大小超过1024字节,可以考虑以下解决方案:
- 使用哈希索引(只适用于相等查询)
- 缩短字段值长度
- 使用部分索引,只索引常用值
- 考虑使用其他查询策略,如全文搜索或正则表达式索引
