bigywater.tech
MongoDB · 29

多键索引:数组字段会生成多少条索引条目

数组字段上的索引会生成多少条索引条目?它占多大?$elemMatch 为什么更快?这篇用 validate 数条目、用 explain 看检查的键数,并量一下写入代价。

3 条
数组 [x,y,z] 在多键索引里生成的条目数;空数组、缺失、标量各 1 条
884 B
每个数组 100 个元素时,平均每个文档在索引里占的字节数(1 个元素只有 11 B)
821 ms
10,000 个文档、每个 100 个元素的批量插入耗时(1 个元素是 24 ms)

一元素一条

实测 · 本机执行输出
  t = ['x','y','z']     索引条目 3
  t = ['x']             索引条目 1
  t = []                索引条目 1
  t = 字段缺失              索引条目 1
  t = 标量 'x'            索引条目 1
  t = ['x','x','x']     索引条目 1
  t = [['x','y'],'z']   索引条目 2
  t = [null, 'x']       索引条目 2
  把这 8 个文档放进同一个集合建索引 t_1,条目数 = 12(上面逐个相加 = 12)
  test> db.a.find({t: "x"}).explain("executionStats")
  {
    isMultiKey: true,
    multiKeyPaths: { t: [ 't' ] },
    keysExamined: 5,
    nReturned: 5
  }

数组字段的索引按元素生成条目,同一个文档里重复的元素只算一条;空数组、字段缺失、标量各一条。explain 里 isMultiKey 为 true。

线性增长

实测 · 本机执行输出
  每个数组元素数               索引条目          索引大小(字节)        每个文档平均
  1                   10,000           114,688          11 B
  5                   50,000           499,712          50 B
  20                 200,000         1,908,736         191 B
  100              1,000,000         8,835,072         884 B

索引条目数和大小随数组长度线性增长:每个数组 100 个元素的 10,000 个文档,索引 8.8 MB,是 1 个元素时的 77 倍。

重复值

实测 · 本机执行输出
  [a,a,a,b] 一个文档,索引条目数 = 2(元素 4 个,不同值 2 个)

数组里重复的值在索引里只存一条,[a,a,a,b] 只有 2 条。

查询

实测 · 本机执行输出
  test> db.p.find({tags: "a3"})
  {
    plan: 'FETCH > IXSCAN(tags_1)',
    nReturned: 5000,
    keysExamined: 5000,
    docsExamined: 5000,
    ms: 4
  }
  test> db.p.find({tags: {$all: ["a3", "b2"]}})
  {
    plan: 'FETCH > IXSCAN(tags_1)',
    nReturned: 714,
    keysExamined: 5000,
    docsExamined: 5000,
    ms: 3
  }
  test> db.p.find({tags: ["a3", "b2", "c0"]})    // 整个数组相等
  {
    plan: 'FETCH > IXSCAN(tags_1)',
    nReturned: 238,
    keysExamined: 5001,
    docsExamined: 5000,
    ms: 3
  }

$all 与整个数组相等的查询只用其中一个元素定界,其余条件在 FETCH 后过滤,所以检查的键数和文档数与单值查询一样多。

$elemMatch

实测 · 本机执行输出
  test> db.p.countDocuments({"items.k": "a3", "items.v": 1})    // 可以由不同元素各满足一个
  2143
  test> db.p.countDocuments({items: {$elemMatch: {k: "a3", v: 1}}})    // 同一个元素同时满足
  1000
  test> db.p.find({"items.k": "a3", "items.v": 1}) 的计划
  {
    plan: 'FETCH > IXSCAN(items.k_1_items.v_1)',
    nReturned: 2143,
    keysExamined: 5000,
    docsExamined: 5000,
    ms: 4
  }
  test> db.p.find({items: {$elemMatch: {k: "a3", v: 1}}}) 的计划
  {
    plan: 'FETCH > IXSCAN(items.k_1_items.v_1)',
    nReturned: 1000,
    keysExamined: 1000,
    docsExamined: 1000,
    ms: 1
  }

普通条件可以由数组里不同的元素各满足一个;$elemMatch 要求同一个元素满足,索引界限更紧,检查的键和文档数只有前者的 1/5。

写入代价

实测 · 本机执行输出
  每个文档   1 个元素:插入 10,000 个文档(带 t 上的索引)耗时 24 ms
  每个文档 100 个元素:插入 10,000 个文档(带 t 上的索引)耗时 821 ms

每个元素都要在索引里插一条,批量插入每个文档 100 个元素比 1 个元素慢了约 34 倍。

分步演示

多键索引的索引条目
本步的对象

    本文用到的工具与边界

    • 本机 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、时间戳等每次运行都不同。条目数来自 validate({full: true}).keysPerIndex;大小来自 collStats 的 indexSizes(fsync 之后读)
    • 没有验证:复合 multikey 索引的 multiKeyPaths 细节与排序;数组套数组的条目数在更深层的表现;multikey 索引上 $size、$in 的界限;multikey 对唯一索引的影响(同一文档内重复不冲突,是否因此需要特别处理);部分索引与 multikey 的组合;写入耗时是否受 journal 影响
    京ICP备2026067502号-1京公网安备11010502063487号