bigywater.tech
MongoDB · 10

BSON 类型的比较顺序:跨类型排序谁在前

一个字段里混着数字、字符串、日期、null,排序谁在前?{$gt: 5} 会不会把字符串也查出来?这篇把 16 种类型放进同一个字段,用 sort、比较运算和 $cmp 逐个验证。

16 种
同一字段里混放的类型数;升序依次是 MinKey、null、数字、字符串、对象、BinData、ObjectId、布尔、日期、Timestamp、正则、MaxKey
只返回 1 个
{v: {$gt: 5}} 在 17 个混合类型文档里只匹配 5.5 一个数字
17 / 17
{v: {$gte: MinKey()}} 命中全部文档,包括没有 v 字段的

跨类型排序

实测 · 本机执行输出
  test> db.t.find({}, {_id: 0, name: 1}).sort({v: 1}).toArray().map(d => d.name)
  [
    'MinKey',       'null',
    '(字段缺失)',   'number -1 (long)',
    'number 5',     'number 5.5 (double)',
    'string "a"',   'string "b"',
    'object {a:1}', 'BinData',
    'ObjectId',     'false',
    'true',         'Date',
    'Timestamp',    'regex',
    'MaxKey'
  ]

MongoDB 对不同类型有固定的排序:MinKey < null < 数字 < 字符串 < 对象 < 数组 < BinData < ObjectId < 布尔 < 日期 < Timestamp < 正则 < MaxKey。数字不区分 int、long、double、decimal,字段缺失与 null 并列。

类型括号

实测 · 本机执行输出
  test> db.t.find({v: {$gt: 5}}, {_id: 0, name: 1}).toArray().map(d => d.name)
  [ 'number 5.5 (double)' ]
  test> db.t.find({v: {$lt: "b"}}, {_id: 0, name: 1}).toArray().map(d => d.name)
  [ 'string "a"' ]
  test> db.t.countDocuments({v: {$gte: MinKey()}}) + " / " + db.t.countDocuments()
  17 / 17
  test> db.t.countDocuments({v: {$lte: MaxKey()}}) + " / " + db.t.countDocuments()
  17 / 17

$gt、$lt 只在同一类型括号内比较,所以 {v: {$gt: 5}} 不会返回字符串或日期,即使它们排在数字后面。MinKey 和 MaxKey 是例外。

null 与缺失

实测 · 本机执行输出
  test> db.t.find({v: null}, {_id: 0, name: 1}).toArray().map(d => d.name)
  [ 'null', '(字段缺失)' ]
  test> db.t.find({v: {$exists: false}}, {_id: 0, name: 1}).toArray().map(d => d.name)
  [ '(字段缺失)' ]
  test> db.t.find({v: {$type: "null"}}, {_id: 0, name: 1}).toArray().map(d => d.name)
  [ 'null' ]
  test> db.t.find({v: {$in: [null]}}, {_id: 0, name: 1}).sort({_id: 1}).toArray().map(d => d.name)
  [ 'null', '(字段缺失)' ]

{v: null} 匹配 null 和缺失两种;要区分用 $exists 或 $type。

数组排序

实测 · 本机执行输出
  test> db.a.find().sort({v: 1}).toArray().map(d => JSON.stringify(d.v))
  [ '[9,0]', '[5,1]', '2', '[3]' ]
  test> db.a.find().sort({v: -1}).toArray().map(d => JSON.stringify(d.v))
  [ '[9,0]', '[5,1]', '[3]', '2' ]

升序排序时数组用最小的元素参与比较,降序用最大的元素,所以 [9,0] 升序排最前、降序也排最前。

子文档

实测 · 本机执行输出
  test> db.o.find().sort({v: 1}).toArray().map(d => JSON.stringify(d.v))
  [
    '{}',
    '{"a":1}',
    '{"a":1,"b":1}',
    '{"a":2}',
    '{"b":1}',
    '{"b":1,"a":1}'
  ]

子文档逐个字段比较类型、字段名、值,字段顺序不同的两个子文档不相等,也排在不同的位置。

$cmp

实测 · 本机执行输出
  test> db.t.aggregate([{$limit: 1}, {$project: {_id: 0, number_vs_string: {$cmp: [1, "a"]}, string_vs_object: {$cmp: ["a", {}]}, object_vs_array: {$cmp: [{}, []]}, array_vs_bool: {$cmp: [[], true]}, null_vs_number: {$cmp: [null, 0]}, int_vs_double: {$cmp: [NumberInt(1), 1.5]}, long_vs_double: {$cmp: [NumberLong("2"), 1.5]}}}])
  [
    {
      number_vs_string: -1,
      string_vs_object: -1,
      object_vs_array: -1,
      array_vs_bool: -1,
      null_vs_number: -1,
      int_vs_double: -1,
      long_vs_double: 1
    }
  ]

聚合里的 $cmp 使用同一套顺序,可以用来快速验证任意两个值谁大;数值类型不同按数值大小比较。

分步演示

跨类型比较顺序
本步的对象

    本文用到的工具与边界

    • 本机 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、时间戳等每次运行都不同。每个类型只放了一两个样本值,排序结果用 mongosh 取回;文档的插入顺序打乱过
    • 没有验证:字符串在不同 collation 下的比较(见后面的 collation 一篇);数组整体比较(不带排序)的细节;BinData 不同子类型和长度的排序;Symbol、JavaScript、DBPointer 等已弃用类型;Decimal128 的 NaN 与 Infinity 的位置;索引键的比较是否与此完全一致
    京ICP备2026067502号-1京公网安备11010502063487号