跳到主要内容

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: updateOnereplaceOne 有什么区别?

答案

  • 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);

这种方式利用索引直接定位起始位置,时间复杂度 O(logn)O(\log n),不受页码大小影响。

Q6: MongoDB 的读关注(Read Concern)有哪些级别?

答案

级别含义
local读取当前节点最新数据(可能未复制到多数节点,可能回滚)
available类似 local,分片集群中可能读到孤儿文档
majority只读取已被多数节点确认的数据(不会回滚)
linearizable线性一致性,读取操作开始前最新的已确认数据
snapshot事务中使用,读取事务开始时的一致性快照

相关链接