bigywater.tech
MongoDB · 25

collation:大小写不敏感比较的代价

想做大小写不敏感的查询,用 collation 还是正则?索引还能不能用?这篇用 20 万个名字对比二进制、collation 查询、匹配与不匹配的索引、正则的耗时和计划,并看 collation 对排序和索引大小的影响。

200,000 → 1
collation 查询的检查文档数:索引 collation 不匹配(全表)对匹配的 collation 索引
0.3 ms
大小写不敏感的等值查询配上同 collation 的索引所需时间(精确查找 0.2 ms)
1.17 倍
同样数据上 collation 索引相对二进制索引的大小

默认二进制

实测 · 本机执行输出
  test> db.names.find({n: "alice"}).toArray().map(d => d.n)
  [ 'alice' ]
  test> db.names.find({}, {_id: 0}).sort({n: 1}).toArray().map(d => d.n)
  [
    'ALICE', 'Alice', 'Bob',
    'Elan',  'Zoe',   'a1',
    'a10',   'a2',    'alice',
    'arger', 'bob',   'zoe',
    'Ärger', 'élan'
  ]

不指定 collation 时按字节比较:大小写不同是不同的值,排序是大写在前、小写在后、带重音的字符最后。

strength

实测 · 本机执行输出
  test> db.names.find({n: "alice"}).collation({locale: "en", strength: 2}).toArray().map(d => d.n)
  [ 'alice', 'Alice', 'ALICE' ]
  test> db.names.find({n: "arger"}).collation({locale: "en", strength: 2}).toArray().map(d => d.n)
  [ 'arger' ]
  test> db.names.find({n: "arger"}).collation({locale: "en", strength: 1}).toArray().map(d => d.n)
  [ 'Ärger', 'arger' ]
  test> db.names.find({}, {_id: 0}).sort({n: 1}).collation({locale: "en"}).toArray().map(d => d.n)
  [
    'a1',    'a10',   'a2',
    'alice', 'Alice', 'ALICE',
    'arger', 'Ärger', 'bob',
    'Bob',   'Elan',  'élan',
    'zoe',   'Zoe'
  ]

collation 的 strength: 2 忽略大小写但区分重音,strength: 1 连重音也忽略;locale 决定排序规则。

numericOrdering 与 zh

实测 · 本机执行输出
  test> db.names.find({n: /^a\d/}, {_id: 0}).sort({n: 1}).collation({locale: "en", numericOrdering: true}).toArray().map(d => d.n)
  [ 'a1', 'a2', 'a10' ]
  test> db.cn.find().sort({n: 1})    // 默认:按 UTF-8 字节
  [
    '刘', '张',
    '李', '杨',
    '王', '赵',
    '陈'
  ]
  test> db.cn.find().sort({n: 1}).collation({locale: "zh"})    // 拼音顺序
  [
    '陈', '李',
    '刘', '王',
    '杨', '张',
    '赵'
  ]

numericOrdering: true 让 a2 排在 a10 前面;locale: "zh" 按拼音排序中文。

索引必须匹配

实测 · 本机执行输出
  test> db.big.find({n: "alice77"}).collation({locale: "en", strength: 2}).explain()    // 索引 n_1 是二进制比较
  {
    plan: 'COLLSCAN',
    nReturned: 1,
    totalKeysExamined: 0,
    totalDocsExamined: 200000
  }
  test> 同样的查询,建了 {n: 1} + collation {locale: "en", strength: 2} 的索引 n_ci 之后
  {
    plan: 'FETCH > IXSCAN(n_ci)',
    nReturned: 1,
    totalKeysExamined: 1,
    totalDocsExamined: 1
  }
  test> 不带 collation 的查询仍然用 n_1 索引
  { plan: 'FETCH > IXSCAN(n_1)', nReturned: 1, totalKeysExamined: 1 }

查询的 collation 必须与索引的 collation 相同才能走索引;否则退化为全表扫描,检查全部 200,000 个文档。

耗时

实测 · 本机执行输出
  精确等值(二进制索引 n_1)                  0.2 ms
  collation strength 2 + 同 collation 索引      0.3 ms
  collation strength 2 但索引 collation 不匹配(全表)     28.8 ms
  正则 /^alice77$/i                        60.1 ms

匹配的 collation 索引下查询 0.3 ms,几乎不比精确查找慢;全表 collation 查询 28.8 ms;正则 /i 60.1 ms,更慢。

集合级默认

实测 · 本机执行输出
  test> db.getCollectionInfos({name: "ci"})[0].options
  {
    collation: {
      locale: 'en',
      caseLevel: false,
      caseFirst: 'off',
      strength: 2,
      numericOrdering: false,
      alternate: 'non-ignorable',
      maxVariable: 'punct',
      normalization: false,
      backwards: false,
      version: '57.1'
    }
  }
  集合里存着 'Erin11980',现在用大小写相反的 'eRIN11980' 去查(查询里没有写 collation):
  test> db.ci.find({n: "eRIN11980"}) 与 db.bin.find({n: "eRIN11980"})
  2 个(ci 集合),0 个(普通二进制集合)
  test> db.ci.getIndexes()
  [
    {
      v: 2,
      key: { _id: 1 },
      name: '_id_',
      collation: {
        locale: 'en',
        caseLevel: false,
        caseFirst: 'off',
        strength: 2,
        numericOrdering: false,
        alternate: 'non-ignorable',
        maxVariable: 'punct',
        normalization: false,
        backwards: false,
        version: '57.1'
      }
    },
    {
      v: 2,
      key: { n: 1 },
      name: 'n_1',
      collation: {
        locale: 'en',
        caseLevel: false,
        caseFirst: 'off',
        strength: 2,
        numericOrdering: false,
        alternate: 'non-ignorable',
        maxVariable: 'punct',
        normalization: false,
        backwards: false,
        version: '57.1'
      }
    }
  ]
  同样 100,000 个文档上的 n_1 索引大小:二进制 1,216,512 字节,collation(en, strength 2) 1,421,312 字节(1.17 倍)

创建集合时指定 collation,查询与索引默认继承;collation 索引比二进制索引大约 17%。

分步演示

collation
本步的对象

    本文用到的工具与边界

    • 本机 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、时间戳等每次运行都不同。耗时是 pymongo 单次查询,预热一次后取 9 次(全表扫描和正则 5 次)中位数;数据是固定种子的随机名字
    • 没有验证:其它 locale 的排序(德语、瑞典语等);caseLevel、caseFirst、alternate 等选项;聚合、$group 与 $lookup 里 collation 的传递;复合索引中部分字段指定 collation;多个 collation 的唯一索引;全文索引与 collation 的关系;ICU 版本变化对已有索引的影响(version 字段)
    京ICP备2026067502号-1京公网安备11010502063487号