MongoDB的复合索引 (Compound Index)
MongoDB 复合索引 (Compound Index) 是指一个索引结构中包含多个字段。它是 MongoDB 性能优化的核心工具之一。
相比于单字段索引,复合索引可以同时支持多个字段的查询、排序和范围查找,但其设计需要遵循特定的规则。
以下是关于复合索引的详细指南,包括原理、最佳实践(ESR 规则)和常见陷阱。
1. 基本语法
创建复合索引时,需要指定包含的字段及其排序方向(1 为升序,-1 为降序)。
javascript
// 创建一个包含 item 和 stock 字段的复合索引
db.products.createIndex({ "item": 1, "stock": -1 })
item: 1: 首先按照item字段升序排列。stock: -1: 在item相同的情况下,按照stock字段降序排列。
2. 核心概念
A. 字段顺序至关重要 (Field Order)
复合索引中字段的顺序决定了索引的结构。
- 例子: 索引
{ a: 1, b: 1 } - 结构: 数据首先按
a排序;只有当a的值相同时,才按b排序。 - 影响: 如果你的查询是
find({ b: "value" }),这个索引是无效的,因为你跳过了a。
B. 索引前缀 (Index Prefixes)
MongoDB 可以使用复合索引的“前缀”来支持查询。
假设索引为:{ a: 1, b: 1, c: 1 }
- 支持的查询:
{ a: ... }{ a: ..., b: ... }{ a: ..., b: ..., c: ... }
- 不支持的查询 (无法利用索引):
{ b: ... }(跳过了头部){ c: ... }{ b: ..., c: ... }
C. 排序方向 (Sort Order)
对于单字段索引,排序方向(1 或 -1)不重要,因为 MongoDB 可以双向遍历。
但在复合索引中,排序方向的组合非常重要。
假设索引:{ a: 1, b: -1 } (a 升序,b 降序)
- 支持的排序:
sort({ a: 1, b: -1 })(完全匹配)sort({ a: -1, b: 1 })(完全反向,MongoDB 可以反向读取索引)
- 不支持的排序 (需要内存排序,性能差):
sort({ a: 1, b: 1 })sort({ a: -1, b: -1 })
3. 最佳实践:ESR 规则 (黄金法则)
设计复合索引时,应遵循 ESR 顺序原则,这能最大化索引利用率:
- E (Equality) - 精确匹配:
- 放在索引的最前面。
- 例如:
find({ status: "active" })中的status。
- S (Sort) - 排序:
- 放在中间。
- 这允许 MongoDB 在扫描索引时直接按需要的顺序获取数据,避免昂贵的内存排序 (Blocking Sort)。
- R (Range) - 范围查询:
- 放在最后。
- 例如:
$gt,$lt,$gte。 - 一旦使用了范围查询,索引中后续的字段就无法用于高效排序了。
ESR 示例场景
假设集合 students,查询:“查找班级为 1 班,分数大于 80 的学生,并按最后更新时间排序”。
javascript
db.students.find({ class_id: 1, score: { $gt: 80 } }).sort({ last_updated: 1 })
- Equality:
class_id(精确匹配 1) - Sort:
last_updated(需要排序) - Range:
score(范围 > 80)
最佳索引设计: { class_id: 1, last_updated: 1, score: 1 }
分析:
- 先定位到
class_id = 1的区域。 - 在该区域内,数据已经按
last_updated排好序了。 - MongoDB 只需要扫描索引,剔除
score <= 80的条目即可。
错误设计: { class_id: 1, score: 1, last_updated: 1 }
- 如果把 Range (
score) 放在 Sort (last_updated) 之前,MongoDB 虽然能快速找到分数大于 80 的记录,但这些记录在索引中并不是按last_updated排序的。因此 MongoDB 必须在内存中重新排序结果,这在数据量大时会导致性能骤降。
4. 复合索引 vs 多个单字段索引
MongoDB 有一种机制叫 索引交集 (Index Intersection),即一个查询可以使用两个单字段索引。
- 查询:
find({ a: 1, b: 1 }) - 现有索引: Index A
{ a: 1 }, Index B{ b: 1 } - 结果: MongoDB 可以 分别扫描 A 和 B,然后计算交集。
- 结论: 虽然可行,但性能通常远不如复合索引
{ a: 1, b: 1 }。复合索引只需要扫描一次。
5. 限制与注意事项
- 字段数量限制: 一个复合索引最多包含 32 个字段。
- 写入性能: 索引越多,插入/更新/删除操作越慢,因为每次数据变动都需要更新索引。不要创建不必要的索引。
- 覆盖查询 (Covered Queries): 如果一个复合索引包含了查询中需要返回的所有字段(Projection),MongoDB 可以直接从索引返回结果,而无需查找实际文档(回表),性能极高。
- 例子: 索引
{ name: 1, age: 1 },查询db.users.find({ name: "Alice" }, { _id: 0, name: 1, age: 1 })。
- 例子: 索引
- 哈希索引: 复合索引中不能包含哈希索引字段(Hashed Index)。
总结
设计 MongoDB 复合索引时,请以此为清单:
- 查询模式优先: 根据实际的查询语句(Find + Sort)来设计。
- 遵循 ESR: 精确匹配 -> 排序 -> 范围查询。
- 注意前缀: 确保索引的字段顺序能覆盖尽可能多的查询场景。
- 检查排序方向: 如果涉及多字段排序,方向必须与索引定义一致(或完全相反)。