bigywater.tech
MongoDB · 11

数字类型混用:int32、int64、double、Decimal128 查询时相等吗

同一个字段里有的存了 int,有的存了 long、double,甚至 Decimal128,查询时算不算相等?大整数会不会丢精度?这篇用真实的 mongod 把相等、比较、唯一索引和聚合的类型提升逐项验证。

4 种
{v: 1} 同时命中 int32、int64、double、Decimal128 存的 1
E11000
唯一索引里已有 1 时,再插入 1.0、NumberLong(1)、Decimal128(1) 都报重复键
2^53
mongosh 里 9007199254740993 这个 JS 数字存进去变成 9007199254740992(double)

数值相等不看类型

实测 · 本机执行输出
  test> db.t.find({v: 1}).toArray()
  [
    { _id: 'int32', v: 1 },
    { _id: 'int64', v: Long('1') },
    { _id: 'double', v: 1 },
    { _id: 'decimal', v: Decimal128('1') }
  ]
  test> db.t.aggregate([{$project: {t: {$type: "$v"}}}])
  [
    { _id: 'int32', t: 'int' },
    { _id: 'int64', t: 'long' },
    { _id: 'double', t: 'double' },
    { _id: 'decimal', t: 'decimal' },
    { _id: 'str', t: 'string' }
  ]
  test> db.t.find({v: NumberLong("1")}).toArray().map(d => d._id)
  [ 'int32', 'int64', 'double', 'decimal' ]

查询 {v: 1} 会命中 int32、int64、double、Decimal128 存的 1,但不命中字符串 "1"。$type 可以看出实际存的类型。

大整数

实测 · 本机执行输出
  test> db.big.find({v: 9007199254740992}).toArray().map(d => d._id)
  [ 'long_2^53', 'double_2^53' ]
  test> db.big.find({v: NumberLong("9007199254740993")}).toArray().map(d => d._id)
  [ 'long_2^53+1' ]
  test> db.big.find({v: 9007199254740993}).toArray().map(d => d._id)    // JS 里这个字面量已经是 9007199254740992
  [ 'long_2^53', 'double_2^53' ]
  test> db.big.aggregate([{$project: {_id: 1, gt: {$gt: ["$v", 9007199254740992]}}}])
  [
    { _id: 'long_2^53', gt: false },
    { _id: 'long_2^53+1', gt: true },
    { _id: 'double_2^53', gt: false }
  ]

服务端比较 int64 与 double 时按精确数值,不会因为 double 精度不够而相等。问题往往出在客户端:mongosh 里写超过 2^53 的 JS 数字,字面量本身已经丢精度,要用 NumberLong("…")。

Decimal128 与 double

实测 · 本机执行输出
  test> db.d.find({v: 0.1}).toArray().map(d => d._id)
  [ 'double 0.1' ]
  test> db.d.find({v: Decimal128("0.1")}).toArray().map(d => d._id)
  [ 'decimal 0.1' ]
  test> db.d.find({v: 0.5}).toArray().map(d => d._id)
  [ 'decimal 0.5', 'double 0.5' ]
  test> db.d.aggregate([{$limit: 1}, {$project: {_id: 0, double_sum: {$add: [0.1, 0.2]}, decimal_sum: {$add: [Decimal128("0.1"), Decimal128("0.2")]}}}])
  [ { double_sum: 0.30000000000000004, decimal_sum: Decimal128('0.3') } ]

0.1 的 double 不等于十进制的 0.1,所以两者互不命中;0.5 在二进制里精确,两种都命中。金额等需要十进制精确的场景用 Decimal128。

唯一索引

实测 · 本机执行输出
  test> db.u.insertOne({v: 1.0})
  E11000 duplicate key error collection: test.u index: v_1 dup key: { v: 1 }
  test> db.u.insertOne({v: NumberLong("1")})
  E11000 duplicate key error collection: test.u index: v_1 dup key: { v: 1 }
  test> db.u.insertOne({v: Decimal128("1")})
  E11000 duplicate key error collection: test.u index: v_1 dup key: { v: 1 }
  test> db.u.insertOne({v: "1"})
  ok

唯一索引按数值判断重复,1、1.0、NumberLong(1)、Decimal128(1) 是同一个值;字符串 "1" 是另一个类型。

类型提升

实测 · 本机执行输出
  test> db.s.aggregate([{$project: {_id: 0, int_plus_int: {$add: ["$a", "$b"]}, t: {$type: {$add: ["$a", "$b"]}}, int_plus_double: {$type: {$add: ["$b", 0.5]}}, int_plus_long: {$type: {$add: ["$b", NumberLong("1")]}}, int_plus_decimal: {$type: {$add: ["$b", Decimal128("1")]}}}}])
  [
    {
      int_plus_int: Long('2147483648'),
      t: 'long',
      int_plus_double: 'double',
      int_plus_long: 'long',
      int_plus_decimal: 'decimal'
    }
  ]
  test> db.m.aggregate([{$group: {_id: null, s: {$sum: "$v"}}}, {$project: {s: 1, t: {$type: "$s"}}}])
  [ { _id: null, s: Long('2147483648'), t: 'long' } ]
  test> db.m2.aggregate([{$group: {_id: null, s: {$sum: "$v"}}}, {$project: {s: 1, t: {$type: "$s"}}}])
  [ { _id: null, s: 3, t: 'int' } ]

int32 加法溢出自动变 int64;遇到 double 结果是 double,遇到 Decimal128 结果是 decimal;没有溢出时保持 int32。

$type 别名

实测 · 本机执行输出
  test> db.t.find({v: {$type: "number"}}).toArray().map(d => d._id)
  [ 'int32', 'int64', 'double', 'decimal' ]
  test> db.t.find({v: {$type: ["int", "long"]}}).toArray().map(d => d._id)
  [ 'int32', 'int64' ]
  test> db.x.insertOne({_id: 1, big: 9007199254740993, big2: NumberLong("9007199254740993")}); db.x.aggregate([{$project: {_id: 0, big: 1, t1: {$type: "$big"}, big2: 1, t2: {$type: "$big2"}}}])
  [
    {
      big: 9007199254740992,
      big2: Long('9007199254740993'),
      t1: 'double',
      t2: 'long'
    }
  ]

"number" 是 int、long、double、decimal 的别名;也可以传数组列出几种类型。

分步演示

数字类型混用
本步的对象

    本文用到的工具与边界

    • 本机 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;为避免 JS 数字精度,大整数一律用 NumberLong("…") 写
    • 没有验证:Decimal128 的 NaN、Infinity、-0 的比较;double 的 NaN 在索引与排序里的位置;驱动在其它语言里对整数的默认类型(pymongo 的 int 会按大小选 int32 或 int64,Java 的 Integer / Long 各不同);$avg、$multiply 等其它算术操作符的类型提升;索引键在不同数值类型下的存储形式
    京ICP备2026067502号-1京公网安备11010502063487号