bigywater.tech
MongoDB · 42

通配符索引:什么场景值得用

文档的键名不固定(attrs 里每个文档带不同的键)时,通配符索引值得用吗?这篇用 10 万个动态键文档,看它能加速什么、不能加速什么,以及索引大小和写入的代价。

291 对 100,000
一个通配符索引下按动态键名查询检查的文档数,对没有索引的全表
266 对 2
两个动态键条件同时出现时,通配符索引与复合索引检查的键数
1.9 倍
带通配符索引时批量插入的耗时是无二级索引的倍数

一个索引覆盖所有键名

实测 · 本机执行输出
  test> db.wc.getIndexes().filter(i => i.name != "_id_")
  [
    {
      v: 2,
      key: { 'attrs.$**': 1 },
      name: 'attrs.$**_1',
      collation: { locale: 'simple' }
    }
  ]
  索引条目数: {'_id_': 100000, 'attrs.$**_1': 300000}(= 100,000 个文档 × 3 个键)

attrs.$** 为 attrs 下的每个子字段生成索引条目(这里每个文档 3 条,共 300,000 条),键是(字段路径,值)。

查询

实测 · 本机执行输出
  test> db.wc.find({"attrs.k17": "v5"})
  {
    plan: 'FETCH > IXSCAN(attrs.$**_1)',
    nReturned: 291,
    keysExamined: 291,
    docsExamined: 291,
    ms: 2
  }
  # 索引键是 (字段路径, 值),所以边界是 [路径, 值]
  test> var e = db.wc.find({"attrs.k17": "v5"}).explain(); var s = e.queryPlanner.winningPlan; while (s.inputStage) s = s.inputStage; ({indexName: s.indexName, indexBounds: s.indexBounds})
  {
    indexName: 'attrs.$**_1',
    indexBounds: {
      '$_path': [ '["attrs.k17", "attrs.k17"]' ],
      'attrs.k17': [ '["v5", "v5"]' ]
    }
  }
  test> db.wc.find({"attrs.k17": {$in: [v5, v6]}})
  {
    plan: 'FETCH > IXSCAN(attrs.$**_1)',
    nReturned: 604,
    keysExamined: 605,
    docsExamined: 604,
    ms: 1
  }
  test> 对照:没有索引的 plain 集合
  {
    plan: 'COLLSCAN',
    nReturned: 291,
    keysExamined: 0,
    docsExamined: 100000,
    ms: 27
  }

按任意动态键名的等值、范围、$in 查询都走索引,检查 291 个文档;没有索引时要扫 100,000 个。

多个条件

实测 · 本机执行输出
  test> db.wc.find({"attrs.k33": "v0", "attrs.k22": "v7"})
  {
    plan: 'FETCH > IXSCAN(attrs.$**_1)',
    nReturned: 2,
    keysExamined: 266,
    docsExamined: 266,
    ms: 0
  }
  test> 对照:cmp 集合上 attrs.k33 + attrs.k22 的复合索引
  {
    plan: 'FETCH > IXSCAN(attrs.k33_1_attrs.k22_1)',
    nReturned: 2,
    keysExamined: 2,
    docsExamined: 2,
    ms: 1
  }

两个动态键条件同时出现时,通配符索引一次只用一个字段的范围(检查 266 个键);复合索引才能同时收窄(2 个键)。

能与不能

实测 · 本机执行输出
  test> find({attrs.k17: {$gte: v1}}).sort({attrs.k17: 1})
  {
    plan: 'FETCH > IXSCAN(attrs.$**_1)',
    nReturned: 5658,
    keysExamined: 5658,
    docsExamined: 5658,
    ms: 3
  }
  test> find({attrs.k17: {$ne: v5}})
  {
    plan: 'COLLSCAN',
    nReturned: 99709,
    keysExamined: 0,
    docsExamined: 100000,
    ms: 19
  }
  test> find({attrs.k17: {$exists: true}})
  {
    plan: 'FETCH > IXSCAN(attrs.$**_1)',
    nReturned: 5944,
    keysExamined: 5945,
    docsExamined: 5944,
    ms: 4
  }
  test> find({name: "item5"})    // name 不在通配符索引覆盖的 attrs.* 下
  {
    plan: 'COLLSCAN',
    nReturned: 1,
    keysExamined: 0,
    docsExamined: 100000,
    ms: 15
  }

同一字段的范围加排序、$exists: true 可以;否定条件($ne)与索引覆盖范围之外的字段不行。

代价

实测 · 本机执行输出
  通配符索引(300,000 条)大小 2,625,536 字节;单字段索引 attrs.k17(约 6,000 条)大小 376,832 字节
  批量插入 100,000 个文档:无二级索引 260 ms;带通配符索引 493 ms(1.9 倍)

300,000 条的通配符索引 2.6 MB,批量插入慢了 1.9 倍。只对真正会查的字段建索引更划算。

限制

实测 · 本机执行输出
  test> createIndex({"attrs.$**": 1}, {unique: true})
  Index type 'wildcard' does not support the 'unique' option
  test> createIndex({"$**": 1}, {wildcardProjection: {"attrs.k1": 0, "name": 0}})    // 排除某些路径
  $**_1
  整个文档 $** 的集合(2 万个文档,每个 name + 3 个 attrs + _id):索引条目数 {'$**_1': 80000, '_id_': 20000}
  test> createIndex({"$**": 1, name: 1})
  Compound wildcard indexes on all fields must also specify 'wildcardProjection' option
  test> createIndex({"attrs.$**": 1, name: 1})    // 通配符路径加普通字段的复合索引
  attrs.$**_1_name_1
  test> db.x5.getIndexes().filter(i => i.name != "_id_")
  [
    {
      v: 2,
      key: { 'attrs.$**': 1, name: 1 },
      name: 'attrs.$**_1_name_1',
      collation: { locale: 'simple' }
    }
  ]

不支持 unique;用 wildcardProjection 控制收录哪些路径;复合通配符索引要求指定路径或 projection。

分步演示

通配符索引
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。10 万个文档,键名随机,值 v0~v19;计划来自 explain("executionStats");写入耗时是一次插入的测量(无二级索引 260 ms,带通配符索引 493 ms)
    • 没有验证:通配符索引上的 $or / $and 组合;数组值与嵌套更深的路径;wildcardProjection 包含(1)与排除(0)混用的限制;分片集合上的通配符索引;通配符索引 vs 多个单字段索引的写入对比;与 sparse / partial 的组合
    京ICP备2026067502号-1京公网安备11010502063487号