MongoDB 的多键索引是什么?数组字段建索引有什么坑?
简化版
MongoDB 对数组字段建索引时会生成多键索引,数组里的每个元素都可能成为索引项。它能加速标签、分类、嵌套数组查询,但会放大索引条目数量,复合多数组索引、排序覆盖和 $elemMatch 边界是高频坑。
详细版
例如:
db.posts.createIndex({ tags: 1 })
db.posts.find({ tags: "mongodb" })
如果 tags 是数组,MongoDB 会为数组元素建立多键索引。查询某个标签很快,但数组越长,索引项越多,写入和存储成本越高。复合索引中如果多个字段都是数组,会产生组合爆炸或受限制。查询同一数组元素的多个条件时,常用 $elemMatch 避免条件匹配到不同数组元素。
完整版教学
一、多键索引来自数组字段
MongoDB 文档天然支持数组。只要对数组字段建索引,这个索引就会成为 multikey index。
db.posts.insertOne({
title: "MongoDB Index",
tags: ["mongodb", "database", "index"]
})
db.posts.createIndex({ tags: 1 })
这时一篇文章可能贡献 3 个索引项,分别对应数组里的 3 个标签。
记忆钩子:数组字段建索引,不是一篇文档一个索引项,而是一个元素一个入口。
二、多键索引适合成员包含查询
多键索引最典型场景是标签、角色、分类、商品属性、用户权限等。
db.posts.find({ tags: "mongodb" })
这个查询可以利用 { tags: 1 } 索引快速找到包含 "mongodb" 的文档。
| 数组字段 | 查询例子 | 是否适合多键索引 |
|---|---|---|
tags | 包含某标签 | 适合 |
roles | 用户是否有角色 | 适合 |
comments | 评论全文搜索 | 不一定,可能太大 |
events | 无限增长日志 | 通常不适合内嵌 |
面试时要强调:多键索引适合数组不太长、查询模式清晰的场景。
三、数组越长,索引放大越明显
如果一个文档有 100 个标签或事件元素,对数组字段建索引后,一个文档可能产生 100 个索引项。
1 document
tags length = 100
index entries ~= 100
如果集合有 100 万文档,每个文档平均 20 个数组元素,理论上可能产生约 2000 万个索引入口。写入、更新数组、删除文档都要维护这些索引项。
所以数组字段不是不能建索引,而是要关注数组长度、更新频率和索引选择性。
四、$elemMatch 解决同一元素匹配问题
数组里是对象时,一个高频坑是多个条件可能匹配到不同元素。
{
scores: [
{ subject: "math", score: 60 },
{ subject: "english", score: 95 }
]
}
如果查询:
db.students.find({
"scores.subject": "math",
"scores.score": { $gte: 90 }
})
这可能表示有一个元素 subject 是 math,另一个元素 score 大于等于 90,并不保证同一个数组元素同时满足。要表达同一元素,应使用 $elemMatch:
db.students.find({
scores: { $elemMatch: { subject: "math", score: { $gte: 90 } } }
})
五、复合多键索引有组合成本
复合索引中包含数组字段时,要非常小心。一个数组字段已经会展开为多个索引项,如果多个数组字段同时参与,可能产生组合爆炸,也可能受到 MongoDB 限制。
// 风险模型:tags 和 categories 都是数组
db.posts.createIndex({ tags: 1, categories: 1 })
更稳妥的方式是结合查询模式重新建模,例如把多值属性拆成子集合、控制数组长度,或者只对最常用数组字段建索引。
索引不是越多越好,多键索引尤其要把写入放大算进去。
六、排序和覆盖查询也有额外边界
多键索引可以支持很多查询,但涉及排序、范围过滤、覆盖查询时,限制比普通标量索引更多。
例如既要按数组字段过滤,又要按时间排序:
db.posts.createIndex({ tags: 1, createdAt: -1 })
db.posts.find({ tags: "mongodb" }).sort({ createdAt: -1 })
这个模式通常合理,因为先按标签定位,再按时间排序。但如果排序字段、范围字段和数组展开组合复杂,就需要用 explain() 看是否出现内存排序、扫描过多或无法覆盖。
面试中可以说:数组索引一定要用实际查询和 explain 验证。
七、常见误区与追问
- 误区:数组字段建索引和普通字段成本一样。 多键索引会为数组元素展开索引项,数组越长放大越明显。
- 误区:多个数组条件天然匹配同一元素。 不一定,同一元素多条件应使用
$elemMatch。 - 误区:标签越多越适合塞数组。 标签过多或更新频繁会导致文档和索引膨胀,需要重新建模。
- 追问:多键索引适合什么场景? 适合短数组、成员包含查询、选择性较好的标签或属性。
- 追问:多键索引为什么影响写性能? 插入、更新、删除一个文档时可能维护多个索引入口。
- 追问:怎么判断数组索引设计是否合理? 看数组长度、查询模式、写入频率,并用
explain()验证扫描量和排序方式。
八、加强记忆
多键索引记成“数组展开成多个索引项”。它让标签、角色、属性包含查询变快,但代价是索引体积和写入维护成本增加。数组对象多条件查询要想到 $elemMatch,复合索引里遇到多个数组要警惕组合成本。最终不要凭感觉,结合数组长度、查询频率和 explain() 判断。