适合人群:环境已经跑起来(第 02 篇),准备正式上手增删改查的开发者。
本文所有示例基于第 02 篇灌入的shop.products四条异构文档,命令可直接粘进 mongosh 执行。系列演示基于 MongoDB 7.x,其中绝大多数能力在 4.x 就已可用。
一、CRUD 的心智模型:条件和动作都是 JSON
MySQL 里增删改查靠四种子句表达;MongoDB 把它们统一成了四个动词方法,而条件和动作本身就是普通文档:
| 你想做的事 | MySQL | MongoDB 单条 | MongoDB 多条 |
|---|---|---|---|
| 插入 | INSERT INTO … VALUES |
insertOne(doc) |
insertMany([docs]) |
| 查询 | SELECT … WHERE |
find(filter, projection) |
find 天然返回多条 |
| 更新 | UPDATE … SET |
updateOne(filter, update) |
updateMany(filter, update) |
| 删除 | DELETE FROM … |
deleteOne(filter) |
deleteMany(filter) |
这张表里藏着一个刻意的设计:凡是名字里没有 Many 的写操作,最多只影响一条文档。初学阶段这其实是安全阀——手抖写出过宽的过滤条件时,updateOne 最坏只改坏一条;反过来,什么时候该升级成 Many 由你显式决定,而不是撞运气。
第二个要点:update 参数分两种形态——操作符更新和整文档替换。这是本文最重要的一条红线,第三节展开。
二、查:find 的操作符体系
2.1 精确匹配、投影与嵌套路径
最朴素的用法直接拿字段值当条件:
1 | db.products.find({ name: "手机 X" }) |
find 的第二个参数是投影,声明只要哪些字段(_id 默认总在,除非置 0):
1 | db.products.find( |
嵌套结构与数组元素用点路径直达:
1 | db.products.find({ "spec.color": "黑" }) // 嵌套字段 |
最后一行的语义容易被误解:传入完整数组是整体相等比较且顺序敏感。想在数组中找”包含某个元素”,直接写元素本身即可(见 2.4)。
2.2 比较操作符:SQL WHERE 的直译
| 操作符 | 含义 | 示例 | 近似 SQL |
|---|---|---|---|
$eq |
等于 | { stock: 120 } |
stock = 120 |
$ne |
不等于 | { stock: { $ne: 0 } } |
stock <> 0 |
$gt / $gte |
大于 / ≥ | { price: { $gte: NumberDecimal("100") } } |
>= 100 |
$lt / $lte |
小于 / ≤ | { stock: { $lt: 50 } } |
< 50 |
$in |
在集合内 | { tags: { $in: ["数码", "图书"] } } |
IN (...) |
$nin |
不在集合内 | { tags: { $nin: ["图书"] } } |
NOT IN (...) |
1 | // 库存少于 50 或价格高于 2000 的商品名 |
顺带一个贯穿全篇的原则再次登场:值的类型必须严丝合缝。{ stock: "120" } 查不到库存为数字 120 的文档——字符串和整数在 BSON 里是两种类型。排查”明明有数据却查不到”的问题时,第一反应就该看类型。
2.3 逻辑组合:$and、$or、$not
同一个字段写两个条件时你其实在用逻辑运算:
1 | db.products.find({ stock: { $gte: 30, $lte: 120 } }) |
隐式 AND 够用的场景居多,但有两种情况必须显式写 $and:同一个键出现多次(如 { $and: [{ price: {$gt: 1} }, { price: {$lt: 9} }] }——其实 JSON 键重复非法,标准写法正是 $and),或者条件需要与其他逻辑运算符混排。它们的对照关系:
| 逻辑需求 | 写法 | 注意点 |
|---|---|---|
| 都满足 | 隐式(逗号并列)或 $and |
同一字段多条件必须 $and |
| 任一满足 | { $or: [cond1, cond2] } |
每个 cond 是独立文档 |
| 取反单个条件 | { price: { $not: { $gt: 100 } } } |
$not 套在操作符上而非字段上 |
| 全都不满足 | { $nor: [cond1, cond2] } |
OR 的否定 |
2.4 null、字段存在与类型
这三个判定是关系库玩家最容易翻车的地方:
1 | db.products.find({ author: null }) |
记住这张三者关系就不再迷惑:null 条件的语义 = 缺失 ∪ 为 null;$exists 只管”有没有”;两者叠加才是严格的”有且有值”。
2.5 数组与嵌套:文档库的主场
数组在第 01 篇说过是”一等公民”,查询端的能力同样完整:
1 | // 包含某元素(整个数组任意位置命中即算) |
$elemMatch 与”拆开的两个条件”有本质区别:后者允许”某元素满足 A、另一个元素满足 B”也算命中。筛选订单明细这类场景里,它是正确性的关键。
三、排序、分页与计数
三个链式方法组成最常见的结果加工管线,而且书写顺序无关——.find().limit().sort() 和 .find().sort().limit() 语义相同,执行顺序固定为 sort → skip → limit:
1 | // 第二页,每页 20 条,价格从高到低 |
两个预警提前打好招呼:
skip深分页是性能黑洞——skip(100000)意味着真的遍历并丢弃十万条。大数据量分页的标准解法(范围游标)留到第 05、07 篇结合聚合与索引细讲;sort的内存上限约 32 MB 且不自动落盘,对无索引大字段乱排海量数据会直接报错——又是索引篇的伏笔。
四、改:替换与更新的生死线
4.1 先看最大的那个坑
update 方法的第二个参数如果不含任何 $ 操作符,就不是局部更新,而是整文档替换:
1 | // 危险示范:意图是"把键盘改成 199 元" |
被覆盖的文档不会报错、不会有警告,一切悄无声息。规避方式只有纪律:任何一次 update 都要默念”我的更新参数里有 $ 吗?”
4.2 更新操作符全家福
真正局部更新的记号库不长,按用途分组记忆:
| 分组 | 操作符 | 一句话 |
|---|---|---|
| 字段赋值 | $set / $unset |
设置 / 移除字段 |
| 数值运算 | $inc / $mul |
原子加减 / 乘,负数即减 |
| 重命名 | $rename |
字段改名 |
| 数组追加 | $push / $addToSet |
尾部插入 / 去重插入 |
| 数组移除 | $pull / $pop |
按条件删 / 删首或尾 |
配合 $push |
$each / $slice / $sort |
批量、截断、排序后再入组 |
1 | // 下单扣减库存:原子自减(配合后续事务章节会更严谨) |
这几个例子共同揭示了一个设计取向:并发安全往往就藏在操作符原子里。$inc 由数据库保证读改写一体,不需要应用层”查出再算再存回”;而那种三步写法放在并发下就是经典的库存超卖来源。
4.3 upsert:查不到就新建
第四个参数(或选项对象)里的 upsert: true 让 update 变成”存在则更新,不存在则按条件+更新指令合成新文档”:
1 | db.views.updateOne( |
浏览量计数、用户偏好项这类”首次访问才建档”的数据天然适合它。
五、删与批量写
删除的操作面很窄,重点全是纪律:
1 | db.products.deleteOne({ name: "试错商品" }) |
先 find 同样的条件、肉眼确认数量,再把 find 换成 deleteMany——这三秒钟的复制粘贴能救回很多个周末。另外 deleteMany({}) 只清空文档,集合本身还在;彻底移除结构要用 drop(),别混用。
真正的生产写入常常是一批混合操作,交给 bulkWrite:
1 | db.products.bulkWrite([ |
ordered: true(默认)遇到中间某条失败即停止,前面的照常生效;ordered: false 则并发打满、跳过失败继续,吞吐更高但语义不同。数据迁移、初始化脚本爱用后者,强依赖先后关系的编排只能选前者。
六、常见坑清单
- 更新忘写
$操作符:静默整文档替换,本章第一大坑; { field: null }的双重语义:连”字段不存在”一起命中,精确判空要$exists+$ne: null;- 类型不匹配查不到数据:
"120"≠120,shell 里肉眼难辨,Compass 的类型徽标是好帮手; - 把 updateOne 当 updateMany 用:名字没 Many 就是只改一条,改动”看起来没生效”多半是这个原因;
- 整数组当条件:
find({ sizes: ["M", "L"] })是顺序敏感的整体相等,找包含请直接给元素或用$all; $regex前导通配绕过索引:{ name: { $regex: /手机$/ } }在没有对应索引支撑时会退化成全扫,模糊查询规模化前先想起第 07 篇。
七、总结与下一步
- CRUD 四动词的条件与动作都是普通文档;One 与 Many 命名即边界,命名之外的区分不存在;
- find 的操作符按”比较 / 逻辑 / 字段判断 / 数组”四象限记忆;
null条件 = 缺失 ∪ 为 null,$exists只问有没有; - 排序分页的链式书写顺序无所谓,执行恒为 sort → skip → limit;skip 深分页是性能伏笔;
- 更新参数没有
$就是整文档替换——把它变成肌肉记忆再谈别的;$inc、$addToSet这类原子操作符同时兼任并发安全的实现手段; - 删除前三秒 find 核对;批量写按是否容忍部分失败来选 ordered 模式。
下一篇:《MongoDB 从零到一(04):数据建模》——回答这个系列最重要的问题:关联数据到底该嵌入还是引用?带着第 02 篇那四份异构商品文档一起进阶。详见 MongoDB 从零到一(04):数据建模。
思考与练习
- 用一条 find 查出”含’数码’或’Java’标签、库存不少于 30”的商品,只返回名称与 tags 字段,并按价格降序。
- 故意演示一次”忘写 $set”的整文档替换(对着自己的测试集合),再用
$set把误删的字段补回来,体会两类代价的差异。- 给
views集合设计一套 upsert 计数方案:同一篇文章的 PV 计数如何保证并发下不丢数?(提示:不用 find 出来再加一。)