CRUD 操作
问题
MongoDB 的 CRUD 操作有哪些?查询操作符和更新操作符怎么用?如何进行投影和排序?
答案
基本 CRUD 操作
MongoDB 的 CRUD 使用方法调用而非 SQL 语句,所有操作都作用于 Collection(集合)上。
创建(Create)
// 插入单个文档
db.users.insertOne({
name: "Alice",
age: 28,
email: "alice@example.com",
tags: ["vip", "active"]
});
// 插入多个文档
db.users.insertMany([
{ name: "Bob", age: 32, email: "bob@example.com" },
{ name: "Charlie", age: 25, email: "charlie@example.com" }
]);
自动生成 _id
如果文档没有 _id 字段,MongoDB 会自动生成一个 ObjectId 作为主键。也可以手动指定任意类型的 _id,只要保证集合内唯一即可。
查询(Read)
// 查找所有文档
db.users.find();
// 条件查询
db.users.find({ age: { $gte: 25 } });
// 查询单个文档
db.users.findOne({ name: "Alice" });
// 投影(只返回指定字段)
db.users.find(
{ age: { $gte: 25 } },
{ name: 1, email: 1, _id: 0 } // 1=包含,0=排除
);
// 排序 + 分页
db.users.find()
.sort({ age: -1 }) // -1 降序,1 升序
.skip(10)
.limit(5);
更新(Update)
// 更新单个文档
db.users.updateOne(
{ name: "Alice" }, // 过滤条件
{ $set: { age: 29 } } // 更新操作
);
// 更新多个文档
db.users.updateMany(
{ age: { $lt: 30 } },
{ $set: { status: "young" } }
);
// 替换整个文档(除了 _id)
db.users.replaceOne(
{ name: "Alice" },
{ name: "Alice", age: 29, role: "admin" }
);
// 查找并更新(原子操作,返回更新前/后的文档)
db.users.findOneAndUpdate(
{ name: "Alice" },
{ $inc: { loginCount: 1 } },
{ returnDocument: "after" } // 返回更新后的文档
);
删除(Delete)
// 删除单个
db.users.deleteOne({ name: "Alice" });
// 删除多个
db.users.deleteMany({ status: "inactive" });
// 查找并删除(返回被删除的文档)
db.users.findOneAndDelete({ name: "Bob" });
查询操作符
比较操作符
| 操作符 | 含义 | 示例 |
|---|---|---|
$eq | 等于 | { age: { $eq: 25 } } |
$ne | 不等于 | { status: { $ne: "inactive" } } |
$gt / $gte | 大于 / 大于等于 | { age: { $gte: 18 } } |
$lt / $lte | 小于 / 小于等于 | { price: { $lt: 100 } } |
$in | 在数组中 | { status: { $in: ["active", "vip"] } } |
$nin | 不在数组中 | { role: { $nin: ["banned"] } } |
逻辑操作符
// $and — 所有条件都满足
db.users.find({
$and: [
{ age: { $gte: 18 } },
{ status: "active" }
]
});
// 简写:同一层级多个条件默认就是 AND
db.users.find({ age: { $gte: 18 }, status: "active" });
// $or — 任一条件满足
db.users.find({
$or: [
{ age: { $lt: 18 } },
{ role: "admin" }
]
});
// $not — 条件取反
db.users.find({ age: { $not: { $gte: 30 } } });
// $nor — 所有条件都不满足
db.users.find({
$nor: [
{ status: "banned" },
{ age: { $lt: 18 } }
]
});
数组操作符
// $elemMatch — 数组中至少有一个元素满足所有条件
db.products.find({
reviews: {
$elemMatch: { rating: { $gte: 4 }, verified: true }
}
});
// $all — 数组包含所有指定元素
db.products.find({
tags: { $all: ["electronics", "sale"] }
});
// $size — 数组长度
db.users.find({ tags: { $size: 3 } });
元素操作符
// $exists — 字段是否存在
db.users.find({ phone: { $exists: true } });
// $type — 字段类型
db.users.find({ age: { $type: "number" } });
正则与文本
// 正则匹配
db.users.find({ name: { $regex: /^ali/i } });
// 全文搜索(需要先创建文本索引)
db.articles.createIndex({ title: "text", content: "text" });
db.articles.find({ $text: { $search: "MongoDB 教程" } });
更新操作符
字段更新
| 操作符 | 作用 | 示例 |
|---|---|---|
$set | 设置字段值 | { $set: { name: "Alice" } } |
$unset | 删除字段 | { $unset: { tempField: "" } } |
$inc | 数值递增 | { $inc: { age: 1, score: -5 } } |
$mul | 数值乘以 | { $mul: { price: 1.1 } } |
$min | 取更小值 | { $min: { lowScore: 50 } } |
$max | 取更大值 | { $max: { highScore: 100 } } |
$rename | 重命名字段 | { $rename: { "old": "new" } } |
$currentDate | 设置为当前日期 | { $currentDate: { updatedAt: true } } |
数组更新
// $push — 向数组追加元素
db.users.updateOne(
{ _id: userId },
{ $push: { tags: "premium" } }
);
// $push + $each + $sort + $slice — 维护有序定长数组
db.products.updateOne(
{ _id: productId },
{
$push: {
recentReviews: {
$each: [{ text: "很好", rating: 5, date: new Date() }],
$sort: { date: -1 }, // 按日期降序
$slice: 10 // 只保留最新 10 条
}
}
}
);
// $pull — 移除满足条件的元素
db.users.updateOne(
{ _id: userId },
{ $pull: { tags: "inactive" } }
);
// $addToSet — 不存在才添加(去重)
db.users.updateOne(
{ _id: userId },
{ $addToSet: { tags: "vip" } }
);
// $pop — 移除首个或末尾元素
db.users.updateOne(
{ _id: userId },
{ $pop: { tags: 1 } } // 1=末尾,-1=首个
);
// $ 定位符 — 更新数组中匹配的第一个元素
db.orders.updateOne(
{ _id: orderId, "items.productId": "P001" },
{ $set: { "items.$.qty": 3 } } // $ 指向匹配的数组元素
);
// $[] 全部定位符 — 更新数组所有元素
db.orders.updateOne(
{ _id: orderId },
{ $inc: { "items.$[].price": 10 } } // 所有 item 涨价 10
);
// $[<identifier>] 过滤定位符 — 按条件更新
db.orders.updateOne(
{ _id: orderId },
{ $set: { "items.$[elem].discount": 0.8 } },
{ arrayFilters: [{ "elem.price": { $gte: 100 } }] }
);
Upsert 模式
upsert: true 表示「不存在就插入,存在就更新」,是一种非常实用的原子操作:
db.pageViews.updateOne(
{ url: "/home", date: "2024-01-15" },
{
$inc: { views: 1 },
$setOnInsert: { createdAt: new Date() } // 仅在插入时设置
},
{ upsert: true }
);
$setOnInsert 只在 upsert 触发插入操作时才生效,更新时不执行。
Bulk Write 批量操作
当需要执行大量写操作时,使用 bulkWrite 减少网络往返:
db.products.bulkWrite([
{ insertOne: { document: { name: "新商品", price: 99 } } },
{ updateOne: {
filter: { name: "键盘" },
update: { $inc: { stock: -1 } }
}},
{ deleteOne: { filter: { status: "discontinued" } } }
], { ordered: false }); // ordered: false 允许并行执行,性能更好
ordered vs unordered
ordered: true(默认):按顺序执行,遇到错误停止ordered: false:并行执行,遇到错误继续处理其他操作
常见面试问题
Q1: find() 和 findOne() 的区别?
答案:
find()返回一个 游标(Cursor),可以遍历所有匹配文档,支持.sort()、.limit()、.skip()等链式操作findOne()返回 单个文档(或null),等价于find().limit(1)的第一个结果
Q2: updateOne 和 replaceOne 有什么区别?
答案:
updateOne使用 更新操作符($set、$inc等)修改特定字段,其他字段不变replaceOne用新文档 整体替换 匹配的文档(_id不变),未包含的字段会被删除
// 原始文档:{ _id: 1, name: "Alice", age: 28, email: "..." }
// updateOne — 只改 age,name 和 email 不受影响
db.users.updateOne({ _id: 1 }, { $set: { age: 29 } });
// 结果:{ _id: 1, name: "Alice", age: 29, email: "..." }
// replaceOne — 整个替换,email 字段丢失
db.users.replaceOne({ _id: 1 }, { name: "Alice", age: 29 });
// 结果:{ _id: 1, name: "Alice", age: 29 } ← email 没了!
Q3: $push 和 $addToSet 区别?
答案:
$push:无条件追加,即使元素已存在也会重复添加$addToSet:仅在元素不存在时才添加,天然去重
// 当前 tags: ["a", "b"]
{ $push: { tags: "a" } } // → ["a", "b", "a"](重复了)
{ $addToSet: { tags: "a" } } // → ["a", "b"](不变)
{ $addToSet: { tags: "c" } } // → ["a", "b", "c"](新增)
Q4: MongoDB 的写关注(Write Concern)是什么?
答案:
Write Concern 控制写操作的确认级别,决定了 数据安全性 和 写入延迟 之间的权衡:
| 级别 | 含义 | 安全性 | 性能 |
|---|---|---|---|
w: 0 | 不等确认(fire-and-forget) | 最低 | 最快 |
w: 1 | 主节点确认写入内存 | 中等 | 快 |
w: "majority" | 多数节点确认 | 高 | 较慢 |
j: true | 写入 journal 日志后确认 | 很高 | 慢 |
生产环境推荐 { w: "majority", j: true },确保数据不丢。
Q5: 如何高效实现分页?skip + limit 有什么问题?
答案:
skip + limit 的问题:skip(N) 需要遍历并丢弃前 N 条文档,当 N 很大时(比如第 10000 页),性能急剧下降。
优化方案 — 基于游标的分页:利用上一页最后一条记录的排序字段值作为下一页的起始条件:
// 第一页
db.orders.find()
.sort({ _id: 1 })
.limit(20);
// 下一页(用上一页最后一条的 _id 作为起点)
db.orders.find({ _id: { $gt: lastId } })
.sort({ _id: 1 })
.limit(20);
这种方式利用索引直接定位起始位置,时间复杂度 ,不受页码大小影响。
Q6: MongoDB 的读关注(Read Concern)有哪些级别?
答案:
| 级别 | 含义 |
|---|---|
local | 读取当前节点最新数据(可能未复制到多数节点,可能回滚) |
available | 类似 local,分片集群中可能读到孤儿文档 |
majority | 只读取已被多数节点确认的数据(不会回滚) |
linearizable | 线性一致性,读取操作开始前最新的已确认数据 |
snapshot | 事务中使用,读取事务开始时的一致性快照 |