bigywater.tech
MongoDB · 30

两个数组字段的复合索引为什么报错

为什么两个数组字段的复合索引会报错?这篇用真实的错误逐个验证哪些情况允许、哪些拒绝,再对比两种绕开办法的查询代价。

171
并行数组的错误码 CannotIndexParallelArrays,插入、更新、建索引都会触发
6000 → 1714
办法一(单字段索引)与办法二(pairs 子文档复合索引)检查的键数,返回都是 1714
120,000
展开成 pairs 后 30,000 个文档的复合索引条目数(每个文档 4 条)

一个数组可以

实测 · 本机执行输出
  test> db.createCollection("t"); db.t.createIndex({a: 1, b: 1})
  a_1_b_1
  test> db.t.insertOne({_id: 1, a: [1, 2, 3], b: "x"})
  { acknowledged: true, insertedId: 1 }
  test> db.t.insertOne({_id: 2, a: 1, b: ["x", "y"]})
  { acknowledged: true, insertedId: 2 }

复合索引里同一个文档最多只有一个被索引的字段是数组;一个数组加一个标量没问题。

两个数组不行

实测 · 本机执行输出
  test> try { db.t.insertOne({_id: 3, a: [1, 2], b: ["x", "y"]}) } catch (e) { ({code: e.code, name: e.codeName, message: e.message}) }
  {
    code: 171,
    name: undefined,
    message: 'cannot index parallel arrays [b] [a]'
  }
  test> db.t.updateOne({_id: 1}, {$set: {b: ["p", "q"]}})    // 把标量改成数组,让 a 和 b 同时是数组
  {
    code: 171,
    message: 'Plan executor error during update :: caused by :: cannot index parallel arrays [b] [a]'
  }
  test> db.t.find().toArray()
  [
    { _id: 1, a: [ 1, 2, 3 ], b: 'x' },
    { _id: 2, a: 1, b: [ 'x', 'y' ] }
  ]

插入或更新让两个字段同时是数组,报 171 cannot index parallel arrays。索引不会展开成笛卡尔积,这样条目数会爆炸。

已有数据

实测 · 本机执行输出
  test> try { db.u.createIndex({a: 1, b: 1}) } catch (e) { ({code: e.code, name: e.codeName, message: e.message.slice(0, 150)}) }
  {
    code: 171,
    name: 'CannotIndexParallelArrays',
    message: 'Index build failed: 420fa910-bf98-4c68-8826-cd95ea44c52b: Collection test.u ( e3a2d792-34d6-4983-b6e4-ada0dc9c5920 ) :: caused by :: collection scan s'
  }
  test> db.u.getIndexes().map(i => i.name)
  [ '_id_' ]

集合里已经有这样的文档时,建索引直接失败,索引不会被创建。

路径决定

实测 · 本机执行输出
  test> db.createCollection("s"); db.s.createIndex({"x.a": 1, "x.b": 1})
  x.a_1_x.b_1
  test> db.s.insertOne({x: [{a:1,b:2},{a:3,b:4}]})    // 同一个数组 x 的两个子字段,只算一个数组
  { acknowledged: true, insertedId: 1 }
  test> db.s.insertOne({x: [{a: [1,2], b: [3,4]}]})    // x.a 与 x.b 都是数组里的数组
  cannot index parallel arrays [b] [a]

同一个数组的两个子字段(x.a、x.b,x 是数组)只算一个数组;各自是数组才是并行数组。

办法一

实测 · 本机执行输出
  test> db.v.find({a: 3, b: 2})    // a、b 各一个单字段 multikey 索引
  {
    plan: 'FETCH > IXSCAN(a_1)',
    nReturned: 1714,
    keysExamined: 6000,
    docsExamined: 6000,
    ms: 8
  }
  test> db.v.getIndexes().map(i => i.name)
  [ '_id_', 'a_1', 'b_1' ]
  test> db.v.createIndex({a: 1, b: 1})    // 两个都是数组的集合上建复合索引
  MongoServerError: Index build failed: 653a4a12-6145-4a4d-833d-c9bcc2f7ac83: Collection test.v ( d76cba83-4382-44ea-947b-433e6070a430 ) :: caused by :: collection scan sto

拆成两个单字段 multikey 索引,查询只能用其中一个,另一个条件在 FETCH 后过滤(检查 6000 个键)。

办法二

实测 · 本机执行输出
  test> db.w.find({pairs: {$elemMatch: {a: 3, b: 2}}})    // 把 a、b 的组合展开成 pairs 子文档数组
  {
    plan: 'FETCH > IXSCAN(pairs.a_1_pairs.b_1)',
    nReturned: 1714,
    keysExamined: 1714,
    docsExamined: 1714,
    ms: 4
  }
  v 集合每个文档的 a、b 各 2 个元素;w 集合把它们展开成 2 × 2 = 4 个 {a, b} 子文档:
  test> db.w.validate({full: true}).keysPerIndex
  { _id_: 30000, 'pairs.a_1_pairs.b_1': 120000 }

把组合展开成子文档数组,用 $elemMatch 查询,界限精确(1714 个键),代价是索引条目是文档数的 4 倍。

分步演示

两个数组字段的复合索引
本步的对象

    本文用到的工具与边界

    • 本机 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、时间戳等每次运行都不同。所有错误信息来自真实的 mongod;数据是固定公式生成的 30,000 个文档
    • 没有验证:并行数组检查在 $set 到子文档数组时的所有路径;分片键或唯一索引上的并行数组;wildcard 索引对此的放宽;展开成 pairs 之后更新一个元素的写放大;三个数组字段的组合
    京ICP备2026067502号-1京公网安备11010502063487号