本节摘要:CRUD 指文档的增查改删四类操作,核心命令是 insertOne、find、updateOne、deleteOne 及其批量版本。本节从一次整集合被清空的事故讲每类操作的安全边界,重点是更新操作符与"先查后删"的纪律。
工程师在 mongosh 里想删某个测试用户,敲了:
db.users.deleteMany({ userId: "u1001" }) // 返回 deletedCount: 4821103
条件里 userId 实际字段名是 uid,匹配了 0 个条件语义变成"匹配所有文档"?不——更隐蔽的版本是:他在两个窗口间复制粘贴时丢掉了过滤参数,执行了 deleteMany({})。集合三秒清空,且当时没有备份。
// 第一步:先数一数要删多少 db.users.countDocuments({ uid: "u1001" }); // 期望值是 1 // 第二步:同样的条件删 db.users.deleteOne({ uid: "u1001" }); // 生产环境更稳的做法:先"软删"再物理清理 db.users.updateOne({ uid: "u1001" }, { $set: { deletedAt: new Date() } });
⚠️ deleteMany 与 updateMany 的空过滤
{}合法且匹配全集合。任何批量写操作前,先 countDocuments 同条件一遍,数字不符就停手。
// $set 只改字段,其余保留 —— 常用 db.users.updateOne({ uid: "u1001" }, { $set: { level: 3 } }); // 整体替换:不带操作符,文档被整个换掉,忘写字段=丢数据 —— 慎用 db.users.replaceOne({ uid: "u1001" }, { uid: "u1001", level: 3 }); // 原子自增与数组维护 db.posts.updateOne({ pid: "p9" }, { $inc: { views: 1 } }); db.posts.updateOne({ pid: "p9" }, { $addToSet: { tags: "nosql" } });
addToSet 去重追加、push 直接追加、$unset 删字段——操作符家族是 MongoDB 更新的全部语义,不要用"读出来改完再写回"的方式更新,那会丢并发。
db.orders.find( { status: "paid", amount: { $gt: NumberDecimal("100") } }, { orderId: 1, amount: 1, _id: 0 } ).sort({ createdAt: -1 }).limit(20);
第一个参数是过滤,第二个是投影(只取需要的字段)。$gt/$lt/$in/$ne 是比较族,$and/$or/$not 是逻辑族。字段名拼错不会报错、只会匹配不到——这正是档案 05 的第一层诱因。
| 操作 | 风险点 | 纪律 |
|---|---|---|
| insert | 类型随手写 | 金额/日期类型先定 |
| find | 字段名拼错静默空结果 | 先 sample 一条看字段 |
| update | replace 语义丢字段 | 优先 $set |
| delete | 空过滤全删 | 先 count 再删 |
💡 mongosh 里可以
db.users.findOne()先看真实字段名再写条件,十秒钟的习惯能挡住大部分误操作。
集合清空后的两小时是标准的灾难响应。第一件事是摘掉应用写入流量,防止新数据混入旧快照;第二件事确认恢复点——幸好该集合有凌晨的全量备份加 oplog 增量,最终恢复到删除前 40 秒的位置,只丢了少量可由上游重放的写入。恢复期间团队复盘出三层防线全部失守:没有开启延迟从库(第 6 章的 hidden delayed member 本可以让恢复变成一次简单回放);mongosh 直连生产没有只读约束;删除操作没有走变更流程。
事后落地的制度很简单:生产 shell 会话默认加只读包装,需要写操作时显式解锁;所有 deleteMany 必须先在工单里粘贴 countDocuments 结果;高价值集合配置 1 小时延迟的隐藏节点。三层防线针对的是同一类人因失误——不信任任何人在疲劳时的复制粘贴。
CRUD 不只是正确性问题,批量写的方式直接影响线上表现。逐条 insertOne 一万次,往返开销占大头;换成 ordered: false 的 bulkWrite,让服务器并行执行且遇错不停,耗时通常能降一个数量级:
const ops = rows.map(r => ({ insertOne: { document: r } })); db.orders.bulkWrite(ops, { ordered: false }); // BulkWriteResult { insertedCount: 10000, writeErrors: [] } // ordered 默认 true:中途出错即停,剩余操作不执行
insertMany 同理。默认有序模式保证顺序但也放弃了并发,导入场景几乎都该关掉。updateMany 的另一个性能细节是写关注(第 6 章详述):单实例上它影响的是是否等 journal 刷盘,写入吞吐与持久性保证之间的旋钮就在这里。
读侧的三个常用修饰符也容易用错。sort 的字段没有索引时,内存排序上限默认 100MB,超限直接报错而不是降级,第 4 章的内存超限事故是同一族问题;limit 与 skip 做深分页时,skip 十万条意味着扫描并丢弃十万条,正确写法是记住上一页末尾的排序键做条件翻页;projection 少取一个数组大字段,网络与反序列化开销可能差几十倍,列表页永远只取展示所需字段。
// 深分页:错误写法 vs 键集翻页 db.feeds.find().sort({ _id: -1 }).skip(100000).limit(20); // 扫描并丢弃 10 万条 db.feeds.find({ _id: { $lt: lastSeenId } }).sort({ _id: -1 }).limit(20); // 直接定位
"取号递增"这类需求,读改写回三步在并发下必然出错,findOneAndUpdate 配合 $inc 和 returnDocument 选项一步完成:
db.counters.findOneAndUpdate( { key: "order" }, { $inc: { seq: 1 } }, { returnDocument: "after", upsert: true } ); // { key: "order", seq: 1042 } ← 并发调用不会拿到重复序号
这也是第 8 章事务出现之前最常用的原子工具,很多场景根本不需要事务,一个原子的更新操作符就够。