MongoDB 部分索引 Partial Index 和稀疏索引 Sparse Index 有什么区别?
简化版
Sparse Index 只索引存在该字段的文档,Partial Index 则可以用过滤条件决定哪些文档进入索引。Partial Index 表达能力更强,通常是新项目里更推荐的选择。
详细版
两者都属于“不是所有文档都进索引”的索引,目的都是减少索引体积、降低写入维护成本,并让某些条件查询更快。
- Sparse Index:只跳过缺失索引字段的文档;如果字段存在但值为
null,通常仍可能进入索引。 - Partial Index:通过
partialFilterExpression指定过滤条件,例如只索引status: "ACTIVE"或deleted: false的文档。 - 唯一约束场景要特别小心:唯一稀疏索引和唯一部分索引都可能让“未进入索引”的文档绕开唯一校验。
- 查询能否使用索引,取决于查询条件是否能推出索引过滤条件;否则优化器不能安全使用部分索引。
- 实战上,如果只是“字段存在才索引”,Sparse Index 可用;如果是软删除、多租户、状态过滤,Partial Index 更清晰。
完整版教学
一、为什么会有“只索引一部分文档”的需求
普通索引会把集合里的每一条文档都维护到索引结构里,查询快了,但写入、更新、删除都要额外维护索引。如果一个集合有 1000 万条订单,其中只有 80 万条是 status: "ACTIVE",而线上查询几乎都只查活跃订单,那么给所有订单建同一个索引就会浪费大量空间。部分索引的思路是:既然业务查询只关心一部分数据,就只让这部分数据进入索引。这样索引页更小,缓存命中率更高,写入维护成本也更低。
db.orders.createIndex(
{ tenantId: 1, createdAt: -1 },
{ partialFilterExpression: { status: "ACTIVE" } }
)
记忆钩子:Sparse Index 解决“字段有没有”的问题;Partial Index 解决“哪些文档值得被索引”的问题。
二、Sparse Index 的机制:字段缺失就不入索引
稀疏索引的规则比较窄:只有索引字段存在的文档才会进入索引。例如给 email 建 sparse index,缺少 email 字段的用户不会进入索引,所以索引体积会比普通索引小。它常用于可选字段,比如不是每个用户都有手机号、不是每个商品都有外部平台 ID。注意它判断的是“字段是否存在”,不是业务语义上的“是否有效”,所以字段存在但为空字符串或 null 时,要结合具体版本和类型理解索引行为,不能想当然地把它当成“非空索引”。
db.users.createIndex(
{ email: 1 },
{ sparse: true }
)
三、Partial Index 的机制:用过滤条件控制入索引范围
Partial Index 比 Sparse Index 更像一个“带 WHERE 条件的索引”。它可以表达字段存在、字段等于某个状态、数值范围、类型匹配等过滤条件,索引只维护满足过滤条件的文档。比如软删除场景里,绝大多数查询都带 deleted: false,就可以只给未删除数据建索引。这样即使历史数据持续增长,热查询用到的索引也不会被大量冷数据拖大。
| 场景 | Sparse Index | Partial Index |
|---|---|---|
| 字段可选 | 适合 | 也可以 |
软删除 deleted:false | 不适合表达 | 很适合 |
| 只索引某状态 | 不适合表达 | 很适合 |
| 条件可读性 | 较弱 | 更强 |
四、查询为什么不一定能用 Partial Index
MongoDB 使用部分索引有一个安全前提:查询条件必须能证明自己只会命中索引覆盖的那部分文档。如果索引只包含 status: "ACTIVE",但查询只有 { tenantId: 7 },优化器不能使用这个索引,因为结果可能还包含非 ACTIVE 文档,而索引里没有它们。只有查询写成 { tenantId: 7, status: "ACTIVE" },或条件逻辑能推出 status: "ACTIVE",这个索引才是完整可靠的候选。这个规则很像关系型数据库里的条件索引:快是快,但查询必须和条件对得上。
// 能使用上面的 partial index
db.orders.find({ tenantId: 7, status: "ACTIVE" }).sort({ createdAt: -1 })
// 通常不能安全使用,因为缺少 status 条件
db.orders.find({ tenantId: 7 }).sort({ createdAt: -1 })
五、唯一约束里最容易踩坑
唯一稀疏索引或唯一部分索引只会约束进入索引的文档,没有进入索引的文档不参与唯一性判断。例如你给 email 建唯一稀疏索引,缺少 email 的用户可以有很多个,因为它们根本没进入索引。再比如只对 { deleted: false } 建唯一索引,那么已删除记录可以和未删除记录有相同业务 key。这个特性有时正好适合软删除,但如果你误以为它约束全集,就会留下重复数据。
db.users.createIndex(
{ tenantId: 1, email: 1 },
{
unique: true,
partialFilterExpression: { deleted: false, email: { $exists: true } }
}
)
六、怎么选:从查询模式反推索引范围
选择时不要先问“Sparse 好还是 Partial 好”,而要先问“我的高频查询是否稳定带某个过滤条件”。如果只是某字段非常稀疏,且查询天然就是按这个字段查,Sparse Index 简单够用。如果业务有软删除、租户状态、审核状态、有效期等语义条件,Partial Index 更可控。设计后一定要用 explain() 验证,不只看有没有索引名,还要看扫描文档数是否明显下降。
1000 万文档:
- 全量复合索引:索引覆盖 1000 万条
- 只索引 ACTIVE:索引覆盖 80 万条
- 如果查询 99% 都查 ACTIVE,索引缓存压力可能下降一个数量级
七、常见误区与追问
- 误区:Sparse Index 就等于非空索引。 它主要关注字段是否存在,不等于业务上的“有效值”,空字符串、null、缺失字段要分开建模。
- 误区:Partial Index 建了查询就一定会用。 查询条件必须能推出部分索引的过滤条件,否则优化器不能用它返回完整结果。
- 误区:唯一部分索引能约束所有文档。 它只约束进入索引的文档,未进入索引的记录可以重复。
- 追问:软删除唯一约束怎么做? 常见做法是对
{ deleted:false }建唯一部分索引,让未删除记录唯一,已删除历史不阻塞新记录。 - 追问:Partial Index 会不会影响写入? 满足过滤条件的写入要维护索引,不满足的写入不用维护该索引,因此整体成本取决于命中比例。
八、加强记忆
这题可以用“门禁”来记:普通索引是所有文档都进门,Sparse Index 的门禁只看“有没有这个字段”,Partial Index 的门禁看“是否满足业务过滤条件”。面试回答时先讲动机:减少索引体积和写入维护成本;再讲差异:Sparse 表达弱,Partial 表达强;最后补上红线:查询必须带得上过滤条件,唯一约束只作用于进索引的数据。这样既能答出概念,也能体现你知道它在生产里会怎么踩坑。