MongoDB 增删改查完整语法
insert/find/update/delete 四类操作的完整语法、常用查询运算符与实战示例拆解。
0. 一句话理解
MongoDB 的增删改查就是四个动词:
insert加、find查、update改、delete删;查询和修改条件都写成 JSON 文档。
1. 插入:insertOne 与 insertMany
db.students.insertOne({
name: "小红",
age: 17,
scores: { math: 92, english: 88 }
})
db.students.insertMany([
{ name: "小刚", age: 18, scores: { math: 70, english: 75 } },
{ name: "小丽", age: 19, scores: { math: 85, english: 95 } }
])讲解:
insertOne插入单条;insertMany接收数组批量插入,比循环单条插入快一个数量级。scores字段内嵌了数学和英语成绩,属于典型的”整份对象一起读写”设计。- 批量插入时若某条违反唯一索引约束,默认整批失败;传
{ ordered: false }可以让成功的继续、只报告失败的那条。
2. 查询:find 与查询运算符
2.1 基础查询
// 查所有
db.students.find({})
// 等值查询
db.students.find({ age: 18 })
// 投影:只要 name 和 age,_id 默认保留
db.students.find({ age: 18 }, { name: 1, age: 1 })讲解:
find({})的空对象表示”无条件”,等价于 SQL 的SELECT *。- 第二个参数是投影:
1表示要的字段,0表示不要的字段(_id: 0可去掉主键)。
2.2 比较运算符
// age 大于 17:$gt 大于、$gte 大于等于、$lt 小于、$lte 小于等于
db.students.find({ age: { $gt: 17 } })
// age 在 17 到 19 之间(含边界)
db.students.find({ age: { $gte: 17, $lte: 19 } })
// name 在指定数组中
db.students.find({ name: { $in: ["小红", "小刚"] } })
// 逻辑组合:age > 18 且 math 成绩 >= 90
db.students.find({
age: { $gt: 18 },
"scores.math": { $gte: 90 }
})讲解:
$gt/$gte/$lt/$lte对应>、>=、<、<=,是查询条件里最常见的四个运算符。$in相当于 SQL 的IN (...)。- 嵌套字段用点路径
"scores.math"访问,注意键名必须加引号,因为包含点号。 - 多个条件写在同一对象里表示”并且”(AND)。
2.3 数组与正则
// courses 数组包含 "数学"
db.students.find({ courses: "数学" })
// name 以 "小" 开头(正则查询)
db.students.find({ name: /^小/ })讲解:
- 数组字段直接写值,表示”包含该元素”,这是文档模型非常实用的特性。
- 正则
/^小/匹配以”小”开头的字符串。带^锚定且大小写敏感的正则可以利用字段索引前缀;一旦写成/小/(不锚定)或忽略大小写(i选项),就只能全集合扫描,数据量大时慎用。
3. 更新:updateOne / updateMany / replaceOne
// 修改第一条匹配的文档:把 age 加 1($inc 自增)
db.students.updateOne(
{ name: "小红" },
{ $inc: { age: 1 } }
)
// 批量修改:所有 18 岁及以上的学生,标记 adult: true
db.students.updateMany(
{ age: { $gte: 18 } },
{ $set: { adult: true } }
)
// 整个文档替换(保留 _id)
db.students.replaceOne(
{ name: "小刚" },
{ name: "小刚", age: 20, remark: "已毕业" }
)讲解:
$set只新增/修改指定字段,$inc做原子自增,$unset删除字段。updateOne只更新第一条匹配;需要更新全部匹配时用updateMany,这两个名字最容易写混。replaceOne用新文档整体替换旧文档(_id不变),适合”整份重写”场景;忘记写上的字段会被丢掉。- 更新操作的第二个参数必须以
$运算符开头(如$set),直接写{ age: 19 }是语法错误。
3.1 数组更新运算符:文档模型的主战场
标量字段的更新就 $set/$inc 两个高频运算符,真正需要专门学习的是数组——因为文档模型把一对多关系放进数组,“往购物车加一件商品""从标签里去掉一个关键词”都是数组更新。核心运算符按用途分三组:
// 第一组:增删元素
db.carts.updateOne(
{ userId: "u-1" },
{ $push: { items: { sku: "m-001", qty: 1 } } } // 尾部追加
)
db.carts.updateOne(
{ userId: "u-1" },
{ $pull: { items: { sku: "m-001" } } } // 删除所有匹配元素
)
db.tags.updateOne(
{ _id: "t-1" },
{ $addToSet: { names: "sale" } } // 去重追加:已存在则不动
)
// 第二组:按位置改元素
db.carts.updateOne(
{ userId: "u-1", "items.sku": "m-001" },
{ $set: { "items.$.qty": 3 } } // $ 指向"查询条件匹配到的那个元素"
)
// 第三组:修剪数组本身
db.carts.updateOne(
{ userId: "u-1" },
{ $push: { logs: { $each: ["a", "b"], $slice: -10 } } } // 追加后只保留最后 10 条
)讲解:
$push与$addToSet的区别是要不要去重:日志、购物车允许重复用$push;标签、收藏集合语义用$addToSet。选错的表现是”列表里出现重复元素”且查不出原因。- 位置运算符
$必须与查询条件配合:查询条件"items.sku": "m-001"定位到数组里的某个元素后,items.$.qty就指向那个元素的qty。没有查询条件就写items.$是运行时错误。 $slice配合$each可以做定长队列(最近 N 条操作日志),比”读出来裁剪再写回去”安全得多——那条路径在并发下会丢更新,而$push + $slice是单文档原子操作。- 底层视角:这些运算符之所以重要,是因为它们都是单文档原子的。两个请求同时
$push同一数组不会互相覆盖;但”读出数组、在应用里改完、整份$set写回”的写法会丢失其中一个请求的修改——并发修改数组时永远优先用运算符,不要用”读-改-写”。
3.2 upsert:查不到就插入
// 更新或插入:没有 u-1 的购物车就先建一条
db.carts.updateOne(
{ userId: "u-1" },
{ $push: { items: { sku: "m-001", qty: 1 } }, $setOnInsert: { createdAt: new Date() } },
{ upsert: true }
)讲解:
- 第三个参数
{ upsert: true }让更新操作在”零条匹配”时自动转为插入,查询条件成为新文档的字段来源。 $setOnInsert里的字段只在插入时生效,更新时不覆盖——createdAt这类”创建时间”字段的标准写法。- 典型场景:计数器(
$inc+ upsert,键不存在就从 0 开始加)、购物车、用户配置项。“先 find 判断再决定 insert 还是 update”的写法在并发下会插入重复文档,upsert 是服务端原子完成的,天然免竞态。
3.3 findOneAndUpdate:改完顺手拿回来
// 原子取号:把计数器加 1,并拿回新值
const r = db.counters.findOneAndUpdate(
{ _id: "order-no" },
{ $inc: { seq: 1 } },
{ returnDocument: "after", upsert: true }
)
print("订单号:NO-" + String(r.seq).padStart(6, "0"))讲解:
updateOne的返回值只告诉你”改了几条”,不返回文档内容;findOneAndUpdate执行更新并返回文档。returnDocument: "after"返回更新后的值(默认是更新前)。取号、发号器、任务队列的”抢占一条任务”(findOneAndUpdate+ 条件{ status: "pending" }+$set: { status: "claimed" })都靠它实现——查询与修改在一次原子操作里完成,这是它存在的全部理由。
4. 删除:deleteOne / deleteMany
// 删除第一条匹配
db.students.deleteOne({ name: "小丽" })
// 删除所有 age 小于 18 的学生
db.students.deleteMany({ age: { $lt: 18 } })
// 清空集合(保留集合本身)
db.students.deleteMany({})讲解:
deleteOne({})会删掉”第一条”文档,而deleteMany({})会清空整个集合——企业环境里误执行后者是常见事故,操作前务必先用find确认条件。- 想连集合一起删掉,使用
db.students.drop()。
5. 排序、分页与计数
// 按 age 降序,再按 name 升序
db.students.find({}).sort({ age: -1, name: 1 })
// 跳过前 2 条,取 5 条(第 3 页,每页 5 条)
db.students.find({}).sort({ age: -1 }).skip(10).limit(5)
// 计数
db.students.countDocuments({ age: { $gte: 18 } })讲解:
sort中1升序、-1降序,多字段按书写顺序依次比较。skip + limit是经典分页写法;数据量极大时深分页性能差,可改用”上一页最后一条的 _id”做游标分页。countDocuments是推荐计数方法,count()已废弃。
6. 动手实践
任务一:商品集合 CRUD。 建 products 集合,批量插入 5 个商品(名称、分类、价格、库存);查询价格在 50-200 之间、按价格降序、只显示名称与价格;把所有库存为 0 的商品价格打 8 折;删除”分类为配件”的商品前先数一数有多少条。提示:折扣用 $mul,删除前的计数用 countDocuments——先数再删是生产习惯。
任务二:并发安全的购物车。 为用户 u-9 实现三步操作:往购物车 $push 两件商品、用位置运算符把其中一件的 qty 改成 3、把数组裁剪成”最多保留 5 条日志”的定长 logs 字段。完成后插入第二条相同商品,观察 $push 与 $addToSet 的结果差异。提示:位置运算符 $ 必须搭配查询条件里对数组元素的匹配。
任务三:取号器。 用 counters 集合实现”订单号发号器”:每次调用号数加 1 并返回新值,格式化成 NO-000001。要求整个过程原子,两个并发请求拿到不同号。提示:findOneAndUpdate + $inc + returnDocument: "after" + upsert 四件套。
先自己写,再对照参考实现:
任务一参考实现
db.products.insertMany([
{ name: "机械键盘", category: "外设", price: 199, stock: 20 },
{ name: "无线鼠标", category: "外设", price: 99, stock: 0 },
{ name: "显示器支架", category: "配件", price: 129, stock: 15 },
{ name: "USB 集线器", category: "配件", price: 59, stock: 0 },
{ name: "电竞耳机", category: "外设", price: 349, stock: 8 }
])
// 范围 + 排序 + 投影一次写全
db.products.find(
{ price: { $gte: 50, $lte: 200 } },
{ name: 1, price: 1, _id: 0 }
).sort({ price: -1 })
// 打 8 折:$mul 是乘法运算符
db.products.updateMany(
{ stock: 0 },
{ $mul: { price: 0.8 } }
)
// 先数再删
db.products.countDocuments({ category: "配件" }) // 例如返回 2
db.products.deleteMany({ category: "配件" })注意 $gte: 50, $lte: 200 写在同一个条件对象里是”区间与”语义;拆成两个并列字段对象也一样,但不能拆成两个 { price: ... } 键重复——JSON 对象同名键会互相覆盖。
任务二与任务三参考实现
// 任务二:购物车
db.carts.updateOne(
{ userId: "u-9" },
{
$push: {
items: { $each: [{ sku: "m-001", qty: 1 }, { sku: "k-002", qty: 1 }] },
logs: { $each: ["add m-001", "add k-002"], $slice: -5 }
}
},
{ upsert: true }
)
// 位置运算符:查询条件定位元素,items.$ 指向它
db.carts.updateOne(
{ userId: "u-9", "items.sku": "m-001" },
{ $set: { "items.$.qty": 3 } }
)
// 去重对比:再 push 一次会出现两条 m-001;addToSet 则只有一条
db.carts.updateOne(
{ userId: "u-9" },
{ $addToSet: { items: { sku: "m-001", qty: 1 } } }
)
// 任务三:发号器(原子、并发安全)
function nextOrderNo() {
const r = db.counters.findOneAndUpdate(
{ _id: "order-no" },
{ $inc: { seq: 1 } },
{ returnDocument: "after", upsert: true }
)
return "NO-" + String(r.seq).padStart(6, "0")
}
nextOrderNo() // NO-000001
nextOrderNo() // NO-000002任务三的关键在”查询与修改同一瞬间完成”:两个并发调用各自触发一次 $inc,MongoDB 对同一文档的写操作串行执行,因此两个号必然不同——换成”先 find 再 update 再加一”的三步写法,并发下两个请求会读到同一个旧值。
7. 一句话记住
条件一律写 JSON:
{ 字段: 值 }是等值,{ 字段: { $gt: 值 } }是比较;改数据记得先查一遍,updateOne/deleteOne只动第一条。