MongoDB 通配符索引 Wildcard Index 是什么?适合动态字段吗?
简化版
Wildcard Index 可以为文档中不固定的字段路径建立索引,适合属性集合很动态、查询字段不可完全预知的场景。但它不是万能索引,索引体积、查询选择性和字段范围都要控制。
详细版
MongoDB 文档模型允许不同文档拥有不同字段,动态属性多时,如果给每个字段单独建索引会难以维护。Wildcard Index 用 $** 表示匹配多个字段路径,让一些动态字段查询也有机会走索引。
- 可以给整个文档建
{ "$**": 1 },也可以限制在某个子文档路径下,如{ "attrs.$**": 1 }。 - 适合商品属性、用户扩展属性、设备上报指标等字段不完全固定的场景。
- 不适合替代所有精心设计的复合索引;高频、固定、排序敏感的查询仍应单独设计索引。
- 字段范围越大,索引越容易膨胀,写入维护成本越高。
- 面试时要强调“动态字段兜底”而不是“万能加速”。
完整版教学
一、动态字段为什么会让普通索引难设计
关系型表结构比较固定,索引字段也比较固定;MongoDB 的文档模型更灵活,问题也随之出现。比如商品集合里,手机有 attrs.memory,鞋子有 attrs.size,冰箱有 attrs.volume,如果每个属性都单独建索引,索引数量会失控。Wildcard Index 的动机就是给这种“字段路径很多、但都在某个动态属性区域内”的查询提供一种兜底索引。它牺牲一部分精确控制,换取对动态字段的覆盖能力。
db.products.createIndex({ "attrs.$**": 1 })
记忆钩子:Wildcard Index 像给“动态属性区”铺一张网,但网越大越重,不能拿它替代所有鱼竿。
二、$** 到底索引了什么
$** 表示对匹配路径下的多个字段建立索引项。如果建 { "attrs.$**": 1 },那么 attrs.color、attrs.size、attrs.memory 这类路径都可能进入索引。查询 { "attrs.color": "black" } 或 { "attrs.memory": "16GB" } 时,就有机会利用这个索引。它索引的是字段路径和值的组合,因此不是简单地把整个 JSON 文档塞进一个索引项,而是把不同路径拆开维护。
文档:
{ attrs: { color: "black", memory: "16GB", weight: 180 } }
可能形成的索引路径:
attrs.color -> "black"
attrs.memory -> "16GB"
attrs.weight -> 180
三、为什么建议限定在子文档路径下
给整个文档建 { "$**": 1 } 看起来省事,但通常太粗。一个业务文档可能有 _id、状态、时间、嵌套数组、审计字段、扩展字段,如果全都进入通配符索引,索引体积会快速膨胀。更合理的方式是把动态字段集中放到 attrs、metadata、extra 这样的子文档里,只对这个区域建 wildcard index。这样既保留灵活性,也把索引成本关在一个可控范围内。
| 建法 | 覆盖范围 | 风险 |
|---|---|---|
{ "$**": 1 } | 全文档所有路径 | 索引膨胀、写入变慢 |
{ "attrs.$**": 1 } | 动态属性区 | 更可控 |
| 普通复合索引 | 固定字段组合 | 维护清晰但不覆盖动态字段 |
四、Wildcard Index 和普通复合索引的边界
通配符索引适合“字段不确定”的过滤条件,不擅长承载复杂排序、强选择性复合条件和非常高频的核心查询。比如电商商品搜索如果 80% 请求都是按 categoryId + status + createdAt 排序,那么这类固定路径应该建普通复合索引。Wildcard Index 可以照顾偶发属性查询,例如用户筛选 attrs.material 或 attrs.noiseLevel。面试时如果说“动态字段都建 wildcard index 就行”,会被继续追问写入成本和排序问题。
// 高频固定查询:更适合普通复合索引
db.products.createIndex({ categoryId: 1, status: 1, createdAt: -1 })
// 偶发动态属性过滤:可用 wildcard index 兜底
db.products.find({ "attrs.noiseLevel": { $lte: 40 } })
五、带数字看索引体积的代价
假设一个集合有 100 万个商品,每个商品平均有 20 个动态属性。如果对 attrs.$** 建通配符索引,理论上可能产生接近 2000 万个属性路径相关索引项。哪怕单个索引项不大,整体写入放大也很明显:每插入一个商品,不是维护 1 个索引键,而可能维护 20 个动态属性索引键。这个数字能帮助你理解为什么 wildcard index 不能无脑覆盖全库。
100 万文档 × 每文档 20 个动态字段 ≈ 2000 万个 wildcard 索引项
如果动态字段增长到 50 个,索引项可能接近 5000 万个
六、落地设计:动态属性也要有边界
使用 Wildcard Index 前,最好先规范动态属性的存放位置、字段类型和查询方式。比如所有商品扩展属性统一放 attrs,不要一会儿写 attr,一会儿写 properties;数值字段不要混用字符串和数字,否则范围查询很难稳定。还要定期看索引使用情况,如果某些动态字段已经变成稳定高频字段,就应该晋升为普通字段并建立专用索引。
db.products.find({ "attrs.weight": { $gte: 100, $lte: 200 } }).explain("executionStats")
七、常见误区与追问
- 误区:Wildcard Index 是万能索引。 它只是覆盖动态路径的手段,不能替代高频固定查询的复合索引设计。
- 误区:给全库
$**建索引最省心。 覆盖范围越大,索引项越多,写入放大和存储成本越明显。 - 误区:动态字段不用治理。 字段名、类型、路径不规范会让索引效果变差,也会让查询条件不可控。
- 追问:商品属性查询适合怎么建? 常见做法是动态属性集中到
attrs,对attrs.$**建索引,再给核心固定查询建普通复合索引。 - 追问:怎么判断是否该改成普通索引? 如果某个动态字段访问频率高、查询模式稳定、排序需求明确,就应该考虑专用索引。
八、加强记忆
把 Wildcard Index 记成“动态字段的兜底网”。它解决的是字段路径不可完全预知的问题,尤其适合扩展属性区;但网铺得越大,维护越重,所以要限制路径、规范字段、保留核心复合索引。面试里讲清这三层:为什么需要、索引了什么、为什么不能滥用,基本就能把这题答扎实。