bigywater.tech
MongoDB · 50

数组更新操作符:$push、$pull、$addToSet、$[]

数组字段怎么更新?$push、$pull、$addToSet 各自做什么?$ 和 $[] 又怎么选?这篇逐个验证,并用 8 个线程对比原子 $push 和读-改-写。

800 对 153
8 线程各 100 次追加后,原子 $push 与读-改-写最终保留的元素数
$[e]
配合 arrayFilters 更新所有满足条件的数组元素
2 个元素
$addToSet 遇到内容相同、字段顺序不同的子文档时数组的元素数

$push 的修饰符

实测 · 本机执行输出
  test> $push: {v: 2}
  [ 3, 1, 2 ]
  test> $push: {v: {$each: [9, 8], $position: 0}}
  [ 9, 8, 3, 1, 2 ]
  test> $push: {v: {$each: [7, 6], $sort: -1, $slice: 4}}    // 追加后降序排序,只保留前 4 个
  [ 9, 8, 7, 6 ]
  test> $push: {v: {$each: [], $slice: -2}}    // 只保留最后 2 个
  [ 7, 6 ]

$each 一次加多个,$position 指定插入位置,$sort 与 $slice 在追加后排序并截断,常用来保留最近 N 条。

删除与去重

实测 · 本机执行输出
  test> $addToSet: {s: "x"}    // 已存在
  0 / ["x","y"]
  test> $addToSet: {s: {$each: ["y","z","w"]}}
  [ 'x', 'y', 'z', 'w' ]
  test> $pull: {n: {$gt: 7}}
  [ 1, 5, 3 ]
  test> $pullAll: {n: [1, 3]}
  [ 5 ]
  test> $pop: {n: 1} 然后 $pop: {n: -1}    // 先删最后一个,再删第一个
  [ 2, 3 ]

$addToSet 有集合语义,$pull 按条件删,$pullAll 按值删,$pop 删头或尾。

位置操作符

实测 · 本机执行输出
  test> {"g.k": "a"} + $set: {"g.$.v": 99}    // 只改第一个 k=a
  [ 'a:99', 'b:1', 'a:1' ]
  test> $inc: {"g.$[].v": 10}    // 所有元素
  [ 'a:109', 'b:11', 'a:11' ]
  test> $set: {"g.$[e].v": 0}, arrayFilters: [{"e.k": "a"}]    // 所有 k=a 的元素
  [ 'a:0', 'b:11', 'a:0' ]
  test> $ 但查询条件里没有数组字段
  Plan executor error during update :: caused by :: The positional operator did not find the match needed from the query.

$ 只改查询匹配到的第一个元素(查询里必须有数组条件);$[] 改全部;$[e] 加 arrayFilters 改满足条件的所有元素。

出错情形

实测 · 本机执行输出
  test> $push 到字符串字段
  Plan executor error during update :: caused by :: The field 's' must be an array but is of type string in document {_id: 1}
  test> $push 到缺失的字段
  [ 1 ]
  test> $pull 不存在的值
  0

对非数组字段 $push 报错;字段缺失时 $push 会创建数组;$pull 不存在的值不报错。

子文档与顺序

实测 · 本机执行输出
  test> $addToSet: {s: {b: 2, a: 1}}    // 内容相同,字段顺序不同
  2 个元素
  test> $addToSet: {s: {a: 1, b: 2}}    // 顺序相同
  2 个元素

$addToSet 比较子文档要求字段顺序一致,内容相同但顺序不同的子文档会被当成新元素加进去。

原子性

实测 · 本机执行输出
  原子的 $push:数组长度 800(应为 800)
  读-改-写:数组长度 153(应为 800,丢掉的是并发时被覆盖的)

单个文档内的 $push 是原子的,8 线程 800 次追加一个不丢;读出来再写回去的做法在并发下大量丢失。

分步演示

数组更新操作符
本步的对象

    本文用到的工具与边界

    • 本机 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、时间戳等每次运行都不同。读-改-写保留的元素数每次运行都不同(这次是 153)
    • 没有验证:$push 到嵌套数组与多层 $[] 的组合;arrayFilters 的多个条件;$addToSet 对嵌套文档字段顺序的处理细节;数组更新在副本集 oplog 里的表示(后面单独一篇);超大数组($push 到几万个元素)的性能
    京ICP备2026067502号-1京公网安备11010502063487号