bigywater.tech
MongoDB · 14

字段顺序:顺序不同的两个文档相等吗

{a:1,b:2} 和 {b:2,a:1} 是同一个文档吗?这篇用同样内容、不同顺序的子文档,验证整体匹配、点号路径、$expr、分组和唯一索引的行为,再看更新操作怎么移动字段。

只匹配 1 个
{sub: {a:1,b:2}} 只匹配字段顺序相同的那一个文档;点号路径匹配则两个都命中
_id 最前
无论发送时 _id 在第几个位置,存储后总是第一个字段
2 个文档
唯一索引上 k={a:1,b:2} 与 k={b:2,a:1} 都能插入,因为被当成不同的值

整体匹配看顺序

实测 · 本机执行输出
  test> db.t.find({sub: {a: 1, b: 2}}).toArray()
  [ { _id: 1, sub: { a: 1, b: 2 } } ]
  test> db.t.find({sub: {b: 2, a: 1}}).toArray()
  [ { _id: 2, sub: { b: 2, a: 1 } } ]

子文档作为一个值比较时,字段顺序是值的一部分:{sub: {a:1, b:2}} 只匹配字段顺序相同的文档。

点号路径与顺序无关

实测 · 本机执行输出
  test> db.t.find({"sub.a": 1, "sub.b": 2}).toArray().map(d => d._id)
  [ 1, 2 ]
  test> db.t.find({$and: [{"sub.b": 2}, {"sub.a": 1}]}).toArray().map(d => d._id)
  [ 1, 2 ]
  test> db.t.find({$expr: {$eq: ["$sub", {a: 1, b: 2}]}}).toArray().map(d => d._id)
  [ 1 ]
  test> db.t.aggregate([{$group: {_id: "$sub", n: {$sum: 1}}}, {$sort: {n: -1}}])
  [ { _id: { b: 2, a: 1 }, n: 1 }, { _id: { a: 1, b: 2 }, n: 1 } ]

{"sub.a": 1, "sub.b": 2} 分别匹配字段,两个顺序都命中;但 $expr 里 $eq 比较整个子文档、$group 用子文档做分组键,都仍然看顺序。

更新怎么移动字段

实测 · 本机执行输出
  test> db.u.updateOne({_id: 1}, {$set: {b: 20}}); db.u.findOne()
  { _id: 1, a: 1, b: 20, c: 3 }
  test> db.u.updateOne({_id: 1}, {$set: {z: 26}}); db.u.findOne()
  { _id: 1, a: 1, b: 20, c: 3, z: 26 }
  test> db.u.updateOne({_id: 1}, {$unset: {a: 1}}); db.u.updateOne({_id: 1}, {$set: {a: 1}}); db.u.findOne()
  { _id: 1, b: 20, c: 3, z: 26, a: 1 }
  test> db.u.updateOne({_id: 1}, {$rename: {z: "y"}}); db.u.findOne()
  { _id: 1, b: 20, c: 3, a: 1, y: 26 }

$set 已有字段保持原位,新字段追加到末尾;$unset 再 $set 或 $rename 会让字段出现在末尾。

_id 在最前

实测 · 本机执行输出
  发送的字段顺序: a, _id, b (用原始 insert 命令)
  test> db.idpos.findOne()
  { _id: 99, a: 1, b: 2 }
  test> db.idpos.replaceOne({_id: 99}, {b: 5, a: 4, _id: 99}); db.idpos.findOne()
  { _id: 99, b: 5, a: 4 }

不管插入或替换时 _id 写在哪,服务端都把它放在文档第一位,其它字段保持发送的顺序。

客户端会改顺序

实测 · 本机执行输出
  test> db.js.insertOne({_id: 1, b: 1, "2": 2, "1": 1, a: 0}); db.js.findOne()
  { '1': 1, '2': 2, _id: 1, b: 1, a: 0 }
  pymongo 读回 mongosh 插入的文档,字段顺序: ['_id', '1', '2', 'b', 'a']
  pymongo 用 SON 按 b, "2", "1", a 的顺序插入,再用 pymongo 读回,字段顺序: ['_id', 'b', '2', '1', 'a']
  test> db.py.findOne()    // 同一个文档,mongosh 显示时又把整数键排到最前
  { '1': 1, '2': 2, _id: 1, b: 1, a: 0 }

mongosh 里的 JavaScript 对象会把像整数的键排到最前面,所以顺序在客户端构造时就变了;Python 用 SON 或保序的字典则不会。

唯一索引

实测 · 本机执行输出
  唯一索引上先后插入 k={a:1,b:2} 和 k={b:2,a:1}:都成功,集合里 2 个文档
  再插入一次 k={a:1,b:2}: E11000 duplicate key error collection: test.g index: k_1 dup key: { k: { a: 1, b: 2 } }, full error: {'index':

唯一索引上,字段顺序不同的子文档是不同的键,不会触发重复键错误。需要唯一性的组合,拆成多个字段建复合唯一索引更可靠。

分步演示

字段顺序
本步的对象

    本文用到的工具与边界

    • 本机 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 的 SON 精确控制,再用 mongosh 与 pymongo 两边读回
    • 没有验证:嵌套多层子文档的 $set 点号路径对顺序的影响;聚合 $addFields / $project 输出的字段顺序;数组里的子文档顺序;复合唯一索引与子文档唯一索引在存储上的差别;其它驱动对字段顺序的处理(Go 的 map 无序等)
    京ICP备2026067502号-1京公网安备11010502063487号