Skip to content

MongoDB 复合索引设计 ​

复合索引是基于多个字段创建的索引,它可以提高多字段查询的性能。在 MongoDB 中,复合索引的字段顺序对查询性能有重要影响,遵循最左前缀原则。

复合索引的特点 ​

1. 字段顺序重要性 ​

复合索引的字段顺序决定了它能支持哪些查询模式。MongoDB 按照索引定义的顺序使用字段,遵循最左前缀原则。

2. 支持多种查询类型 ​

复合索引可以支持:

  • 基于索引前缀的查询
  • 基于所有索引字段的精确匹配查询
  • 基于索引字段的范围查询
  • 基于索引字段的排序操作

3. 覆盖查询支持 ​

当复合索引包含查询所需的所有字段时,可以实现覆盖查询,避免回表访问文档数据,提高查询性能。

4. 索引大小和维护成本 ​

复合索引的大小和维护成本高于单字段索引,需要权衡查询性能和写入性能。

最左前缀原则 ​

原则说明 ​

最左前缀原则是复合索引设计的核心原则,它指的是:

  • 复合索引 { a: 1, b: 1, c: 1 } 可以支持以下查询:
    • { a: ... }
    • { a: ..., b: ... }
    • { a: ..., b: ..., c: ... }
  • 但不支持以下查询:
    • { b: ... }
    • { c: ... }
    • { b: ..., c: ... }

示例说明 ​

javascript
// 创建复合索引
db.orders.createIndex({ customer_id: 1, order_date: -1, total_amount: 1 })

// 支持的查询
db.orders.find({ customer_id: "12345" })  // 匹配最左前缀
db.orders.find({ customer_id: "12345", order_date: { $gt: ISODate("2025-01-01") } })  // 匹配前两个字段
db.orders.find({ customer_id: "12345", order_date: { $gt: ISODate("2025-01-01") }, total_amount: { $gt: 100 } })  // 匹配所有字段

// 不支持的查询
db.orders.find({ order_date: { $gt: ISODate("2025-01-01") } })  // 缺少最左前缀 customer_id
db.orders.find({ total_amount: { $gt: 100 } })  // 缺少最左前缀 customer_id
db.orders.find({ order_date: { $gt: ISODate("2025-01-01") }, total_amount: { $gt: 100 } })  // 缺少最左前缀 customer_id

复合索引设计原则 ​

1. 高频查询优先 ​

将最常用的查询字段放在复合索引的最左边。

2. 高选择性字段优先 ​

选择性是指字段的不同值数量与文档总数的比率。选择性越高的字段,过滤效果越好,应放在复合索引的左边。

3. 范围查询字段后置 ​

将范围查询字段(如 $gt, $lt, $in 等)放在复合索引的右边,因为范围查询之后的字段无法被索引有效使用。

4. 排序字段考虑 ​

如果查询包含排序操作,应将排序字段放在复合索引的合适位置,且排序方向应与索引定义一致。

5. 覆盖查询优化 ​

如果查询只返回少量字段,考虑将这些字段包含在复合索引中,实现覆盖查询。

6. 避免冗余索引 ​

避免创建冗余的复合索引,例如同时创建 { a: 1, b: 1 } 和 { a: 1 },因为前者已经包含了后者的功能。

复合索引设计示例 ​

示例 1:电商订单查询 ​

查询模式:

  • 基于 customer_id 和 order_date 查询订单
  • 基于 customer_id、order_date 和 status 查询订单
  • 按 order_date 降序排序

索引设计:

javascript
db.orders.createIndex({ customer_id: 1, order_date: -1, status: 1 })

说明:

  • customer_id 是高频查询字段,放在最左边
  • order_date 用于范围查询和排序,放在中间,使用降序索引
  • status 是精确匹配字段,放在最右边

示例 2:用户信息查询 ​

查询模式:

  • 基于 country 和 age 查询用户
  • 基于 country、age 和 gender 查询用户
  • 按 age 升序排序
  • 返回 name 和 email 字段

索引设计:

javascript
db.users.createIndex({ country: 1, age: 1, gender: 1, name: 1, email: 1 })

说明:

  • country 是高频查询字段,放在最左边
  • age 用于范围查询和排序,放在中间
  • gender 是精确匹配字段,放在右边
  • name 和 email 用于覆盖查询,避免回表

示例 3:博客文章查询 ​

查询模式:

  • 基于 category 和 publish_date 查询文章
  • 基于 category、publish_date 和 author 查询文章
  • 按 publish_date 降序排序
  • 按 views 降序排序

索引设计:

javascript
db.articles.createIndex({ category: 1, publish_date: -1, author: 1, views: -1 })

说明:

  • category 是高频查询字段,放在最左边
  • publish_date 用于范围查询和排序,放在中间
  • author 是精确匹配字段,放在右边
  • views 用于排序,放在最右边

复合索引与排序 ​

排序方向一致性 ​

当查询包含排序操作时,复合索引的排序方向应与查询的排序方向一致,否则索引无法被有效使用。

示例:排序与索引方向 ​

javascript
// 索引定义
db.orders.createIndex({ customer_id: 1, order_date: -1 })

// 支持的排序
db.orders.find({ customer_id: "12345" }).sort({ order_date: -1 })  // 排序方向与索引一致
db.orders.find({ customer_id: "12345" }).sort({ order_date: 1 })   // 不支持,排序方向与索引相反

// 复合排序示例
db.orders.createIndex({ customer_id: 1, order_date: -1, total_amount: 1 })

// 支持的复合排序
db.orders.find({ customer_id: "12345" }).sort({ order_date: -1, total_amount: 1 })  // 排序方向与索引一致

复合索引与范围查询 ​

范围查询的影响 ​

当复合索引中包含范围查询时,范围查询之后的字段无法被索引有效使用。

示例:范围查询与索引字段顺序 ​

javascript
// 索引定义
db.orders.createIndex({ customer_id: 1, order_date: -1, total_amount: 1 })

// 范围查询在 order_date 字段,total_amount 仍可使用索引
db.orders.find({ customer_id: "12345", order_date: { $gt: ISODate("2025-01-01") }, total_amount: { $gt: 100 } })

// 索引定义
db.orders.createIndex({ order_date: -1, customer_id: 1, total_amount: 1 })

// 范围查询在 order_date 字段,customer_id 和 total_amount 仍可使用索引
db.orders.find({ order_date: { $gt: ISODate("2025-01-01") }, customer_id: "12345", total_amount: { $gt: 100 } })

覆盖查询 ​

覆盖查询的定义 ​

覆盖查询是指查询所需的所有字段都包含在索引中,MongoDB 可以直接从索引返回结果,无需回表访问文档数据。

覆盖查询的条件 ​

  • 查询的所有返回字段都必须是索引的一部分
  • 查询条件中的所有字段都必须是索引的一部分
  • 不能使用 $text 等特殊查询运算符

覆盖查询示例 ​

javascript
// 索引定义
db.users.createIndex({ country: 1, age: 1, name: 1, email: 1 })

// 覆盖查询:只返回 name 和 email 字段
db.users.find(
  { country: "China", age: { $gt: 30 } },
  { name: 1, email: 1, _id: 0 }  // _id 默认返回,需要显式排除
)

// 非覆盖查询:返回未包含在索引中的字段
db.users.find(
  { country: "China", age: { $gt: 30 } },
  { name: 1, email: 1, address: 1, _id: 0 }  // address 不在索引中
)

复合索引的限制 ​

1. 索引大小限制 ​

复合索引的大小受限于 MongoDB 的索引大小限制(单个索引条目不超过 1024 字节)。

2. 字段数量限制 ​

复合索引最多可以包含 32 个字段。

3. 写入性能影响 ​

复合索引会增加写入操作的开销,因为每次写入都需要更新索引。

4. 内存使用 ​

复合索引会占用更多的内存空间,影响 WiredTiger 缓存的效率。

复合索引设计最佳实践 ​

1. 分析查询模式 ​

在设计复合索引之前,分析应用程序的查询模式,确定最常用的查询字段和查询类型。

2. 使用 explain() 分析 ​

使用 explain() 命令分析查询执行计划,验证索引是否被有效使用。

3. 监控索引使用情况 ​

使用 $indexStats 聚合管道监控索引的使用情况,识别未使用的索引。

4. 定期优化索引 ​

定期审查和优化索引,删除未使用的索引,调整索引字段顺序。

5. 考虑分片键设计 ​

如果使用分片集群,复合索引设计应考虑分片键的选择,避免跨分片查询。

6. 测试不同索引设计 ​

在测试环境中测试不同的索引设计,比较它们的性能差异,选择最优方案。

复合索引设计决策树 ​

常见问题(FAQ) ​

Q1: 如何确定复合索引的字段顺序? ​

A1: 确定复合索引字段顺序的方法:

  1. 将最常用的查询字段放在最左边
  2. 将高选择性字段放在最左边
  3. 将精确匹配字段放在范围查询字段左边
  4. 考虑排序操作的需求,保持排序方向一致

Q2: 复合索引和多个单字段索引有什么区别? ​

A2: 复合索引和多个单字段索引的区别:

  • 复合索引可以支持多字段查询,而多个单字段索引只能支持单个字段查询
  • 复合索引遵循最左前缀原则,而多个单字段索引之间没有关联
  • 复合索引可以实现覆盖查询,而多个单字段索引无法实现
  • 复合索引的维护成本低于多个单字段索引

Q3: 什么时候应该使用复合索引? ​

A3: 使用复合索引的场景:

  • 需要基于多个字段进行查询
  • 需要基于多个字段进行排序
  • 需要实现覆盖查询
  • 查询选择性较低的单个字段需要与其他字段组合使用

Q4: 如何避免创建冗余的复合索引? ​

A4: 避免创建冗余复合索引的方法:

  • 检查现有索引,避免创建功能重叠的索引
  • 例如,创建 { a: 1, b: 1 } 后,不需要再创建 { a: 1 }
  • 使用 db.collection.getIndexes() 查看现有索引
  • 使用 $indexStats 监控索引使用情况

Q5: 复合索引如何影响写入性能? ​

A5: 复合索引对写入性能的影响:

  • 每次写入操作都需要更新复合索引
  • 复合索引越大,写入操作的开销越大
  • 对于高写入负载的集合,应谨慎设计复合索引
  • 可以考虑使用部分索引或稀疏索引减少索引大小

Q6: 如何实现复合索引的覆盖查询? ​

A6: 实现复合索引覆盖查询的步骤:

  1. 分析查询,确定返回的字段
  2. 创建包含查询条件和返回字段的复合索引
  3. 在查询中显式排除 _id 字段(如果不需要)
  4. 使用 explain() 验证查询是否使用了覆盖索引

Q7: 复合索引可以支持哪些查询操作符? ​

A7: 复合索引支持的查询操作符:

  • 精确匹配:=
  • 范围查询:$gt, $gte, $lt, $lte
  • 包含查询:$in
  • 存在查询:$exists
  • 正则表达式:$regex(仅前缀匹配)

Q8: 如何监控复合索引的使用情况? ​

A8: 监控复合索引使用情况的方法:

  1. 使用 $indexStats 聚合管道:db.collection.aggregate([{ $indexStats: {} }])
  2. 使用 MongoDB Compass 的性能面板
  3. 使用 db.currentOp() 查看当前查询使用的索引
  4. 使用监控工具如 Prometheus + Grafana

Q9: 复合索引的大小限制是什么? ​

A9: 复合索引的大小限制:

  • 单个索引条目不超过 1024 字节
  • 复合索引最多包含 32 个字段
  • 索引名称长度不超过 128 字符

Q10: 如何在分片集群中设计复合索引? ​

A10: 分片集群中复合索引设计的注意事项:

  1. 考虑分片键的选择,避免跨分片查询
  2. 将分片键包含在复合索引中,提高查询性能
  3. 避免在分片键上创建过多的复合索引
  4. 考虑数据分布的均匀性
  5. 测试跨分片查询的性能