MongoDB的复合索引是把多个字段组合在一起创建的索引,它可以同时加速包含这些字段的等值查询、排序和范围过滤。复合索引设计的难点在于字段顺序:同一个字段集合,顺序不同,索引的扫描范围可能从精确缩小变成全表扫描。因此,理解复合索引的前缀规则和查询执行计划,是设计高效索引的前提。

一、复合索引的字段顺序为什么关键
复合索引在B树中按照字段顺序逐层比较,同一个索引会按照第一个字段排序,第一个字段相同再比较第二个字段,以此类推。这个特性决定了查询只有从最左侧字段开始匹配,才能有效利用索引。例如在orders集合上创建索引{ user_id: 1, status: 1, created_at: -1 }:
db.orders.createIndex({ user_id: 1, status: 1, created_at: -1 })
该索引可以高效支持只包含user_id的查询,因为user_id是索引前缀;但如果查询只包含status作为过滤条件,就无法使用该索引,因为跳过了最左侧的user_id字段。这就是复合索引最核心的前缀规则。
字段顺序还决定排序能否利用索引。如果查询按created_at排序,但created_at不是索引前缀,MongoDB需要对匹配结果做内存排序,当结果集较大时容易触发内存限制。因此,字段顺序需要在查询条件、排序字段和范围条件之间做出权衡。实际设计中,不能只考虑等值过滤,还要同时看排序和范围条件对索引前缀的影响。
二、ESR原则:先等值、再排序、后范围
MongoDB官方推荐的复合索引设计原则可以概括为ESR:Equality、Sort、Range。也就是把等值过滤字段放在最前面,排序字段放在中间,范围查询字段放在最后。这样设计的原理在于:等值字段可以最大程度缩小扫描范围,排序字段利用索引顺序避免内存排序,而范围字段放在最后可以保证索引的前面部分仍然有序。
比如一个典型查询是查询某用户最近30天的订单并按下单时间倒序:
db.orders.find({
user_id: 10086,
created_at: { $gte: ISODate("2025-01-01") }
}).sort({ created_at: -1 })
此时适合创建索引{ user_id: 1, created_at: -1 }。user_id是等值条件,放在前面;created_at既是排序字段也是范围字段,因为同一字段同时承担排序和范围限制,把它放在user_id之后即可。这样查询先通过前缀锁定用户,再按时间顺序读取,不会触发额外的内存排序。
如果范围字段和排序字段不同,例如查询某用户状态为已支付,按下单时间倒序,金额大于100元:
db.orders.find({
user_id: 10086,
status: "paid",
total: { $gt: 100 }
}).sort({ created_at: -1 })
这时索引字段顺序建议为{ user_id: 1, status: 1, created_at: -1, total: 1 }。等值字段user_id和status放在最前端,排序字段created_at放在中间,范围字段total放在最后。这样等值条件命中前缀,排序使用created_at的有序性,范围条件total只过滤剩余数据。如果范围条件放在中间,后面的排序字段就无法借助索引顺序,执行计划中会出现额外的SORT阶段。
需要说明,如果查询中没有排序,或者范围字段的选择性很低,ESR原则仍然适用,但可以结合实际执行计划调整。例如范围字段选择性极高,偶尔也可以将其适当提前,但这通常不是默认选择。设计索引时要结合真实查询模式和返回数据量判断。
三、覆盖索引与执行计划验证
覆盖索引指查询需要的字段全部包含在索引中,MongoDB无需回表读取文档即可返回结果。这在查询投影只包含少量字段时非常有效。例如:
db.orders.find(
{ user_id: 10086, status: "paid" },
{ order_no: 1, created_at: 1, _id: 0 }
)
如果存在索引{ user_id: 1, status: 1, order_no: 1, created_at: 1 },该查询可以完全由索引覆盖,执行计划的totalDocsExamined可能为0,totalKeysExamined等于返回文档数。这能显著降低IO开销,特别适合高并发读场景。
设计覆盖索引时要注意字段顺序依然遵循前缀规则,同时索引大小会随着包含更多字段而增加。不要为了覆盖查询把所有字段都塞进索引,否则写入性能会下降,内存占用也会增大。可以用db.collection.explain("executionStats")查看执行计划,确认是否出现FETCH阶段和SORT阶段。
db.orders.find({ user_id: 10086, status: "paid" })
.sort({ created_at: -1 })
.explain("executionStats")
重点观察winningPlan中的stage。理想情况下是IXSCAN直接返回,没有SORT和FETCH;如果看到COLLSCAN说明索引未命中,需要调整字段顺序或创建新索引。执行计划还能显示totalKeysExamined和totalDocsExamined,这两个指标差值越大,说明回表越频繁,越需要考虑覆盖索引或调整查询条件。
四、常见误区与复合索引最佳实践
第一个误区是认为索引越多越好。每个索引都会占用内存和磁盘,写入时还要同步更新所有相关索引。为低频查询创建大量索引,可能让写入性能下降而查询收益有限。应根据慢查询日志和实际业务优先级创建索引,而不是针对所有可能查询都建索引。
第二个误区是忽略字段基数。对于布尔值、状态码这类低基数字段,单独放在复合索引最前面往往无法有效缩小范围。例如{ status: 1, created_at: -1 }中,如果status只有三种值,该索引对大量数据的过滤效果有限。此时如果查询总带user_id,应把user_id放在最前,status后置,让高基数字段先大幅收窄范围。
第三个误区是忽视排序方向。复合索引的每个字段可以指定升序或降序,只有查询的排序方向与索引定义一致或完全相反时,MongoDB才能高效使用索引排序。例如索引{ user_id: 1, created_at: -1 }支持sort({ user_id: 1, created_at: -1 })和sort({ user_id: -1, created_at: 1 }),但如果按sort({ user_id: 1, created_at: 1 })排序,无法直接利用该索引完成排序,需要额外内存排序。
最佳实践上,建议遵循以下步骤:先收集慢查询,使用explain分析候选索引;按照ESR原则设计字段顺序;优先满足高频、高价值查询;定期检查索引使用情况,删除未使用的索引;对大批量写入场景,可以先删除部分索引,写完后再重建。最后,上线前在测试环境充分验证,避免索引变更导致生产性能波动。MongoDB还支持将索引设为hidden状态,可以先隐藏索引观察性能,确认无影响后再删除,比直接删除更安全。对于复合索引,还可以使用partialFilterExpression创建部分索引,只索引满足特定条件的文档,减少索引体积并提升写入效率。
MongoDB复合索引索引设计查询优化修改时间:2026-08-25 15:13:48