复合索引必须以tenant_id为最左前导字段,否则无法高效过滤单租户数据,易导致全表扫描;正确顺序如{tenant_id:1,created_at:-1},错误顺序如{created_at:-1,tenant_id:1}会使索引失效。

复合索引必须包含租户ID字段
多租户场景下,tenant_id 是天然的查询过滤前缀。如果索引不以 tenant_id 开头,MongoDB 无法高效定位单租户数据,会导致全集合扫描(COLLSCAN)。即使后续字段选择性很高,缺失前置租户约束也会让索引失效。
常见错误是把业务主键(如 order_id)或时间字段(如 created_at)放在最左位,结果所有租户的数据混在一起排序,查询时仍需遍历大量无关记录。
- ✅ 正确顺序:
{ tenant_id: 1, created_at: -1, status: 1 } - ❌ 错误顺序:
{ created_at: -1, tenant_id: 1, status: 1 }(tenant_id不在最左,无法用于范围裁剪) - ⚠️ 注意:即使应用层已确保每次查询都带
tenant_id,MongoDB 也不会“智能推导”其存在,索引结构必须显式体现
按高频查询模式补全字段顺序
租户ID之后的字段顺序,取决于实际 find() 或 aggregate() 中的 filter、sort 和 projection 组合。MongoDB 复合索引遵循“前缀匹配”原则——只有连续从左开始的字段能被用于过滤或排序。
例如,若常查 “某租户下指定状态的最新10条订单”,对应查询为:db.orders.find({ tenant_id: "t1", status: "paid" }).sort({ created_at: -1 }).limit(10),则索引应为:{ tenant_id: 1, status: 1, created_at: -1 }。
- 状态字段
status放在created_at前,才能支持tenant_id + status的等值过滤 -
created_at: -1紧跟其后,使sort()直接复用索引顺序,避免内存排序(in-memory sort) - 若还有分页需求(如
skip(100)),该索引仍有效;但深度分页性能会下降,需考虑游标方案
慎用稀疏索引或部分索引替代租户字段
有人试图用 sparse: true 或 partialFilterExpression 来“节省空间”,比如只对非空 tenant_id 建索引。这在多租户系统中极危险——一旦某文档 tenant_id 为 null 或缺失,它将完全不在索引中,导致查询遗漏或逻辑错误。
租户隔离是强一致性要求,不能靠索引“选择性覆盖”。所有文档必须有明确、非空、不可变的 tenant_id,且索引必须无条件包含它。
- ❌ 避免:
{ tenant_id: 1, ... }, { sparse: true }(tenant_id: null文档不可见) - ❌ 避免:
{ partialFilterExpression: { tenant_id: { $exists: true } } }(同上,且可能掩盖数据质量问题) - ✅ 强制写入校验:在应用层或数据库级(如 MongoDB 5.0+ 的 schema validation)确保
tenant_id字段必填、类型为字符串、长度合规
验证索引是否真正生效
建完索引别只看 explain("executionStats") 里有没有 IXSCAN,重点看三处:
-
executionStats.nReturned是否等于预期结果数(而非远大于) -
executionStats.totalDocsExamined是否接近nReturned(越接近说明裁剪越准) -
executionStats.executionStages.stage是否为IXSCAN,且indexName是你刚建的那个
特别注意:使用 $or、$in 多值查询时,即使带 tenant_id,也可能触发多个索引分支或回表。建议先用单值 tenant_id 测试,再逐步加复杂度。
租户ID字段看似简单,但它决定了整个索引的根基——漏掉它,或者放错位置,后面所有优化都是空中楼阁。


















