资讯详情

资讯详情

行业资讯

MongoDB基础操作全攻略:集合、增删改查、更新语法、索引与排错

MongoDB 一直是我日常排查数据时最顺手的一套数据库早年做日志采集和用户画像那类项目很多临时查询我都是直接对着 mongosh 敲的。最近带几个新同事上手发现大家对 MongoDB 基本操作的认知往往停在“装个库然后 insert/find”的层面一旦聊到集合是何时被创建的、更新文档时到底该用 $set 还是直接整个替换、skip 深分页为什么慢很多人就说不清了。这篇文章我就按实际使用顺序把库和集合、增删改查、更新语法、索引设计、常见报错完整过一遍。适合刚接触 NoSQL 的开发者也适合用过但没来得及系统整理细节的人跟着敲一遍基本就能覆盖大部分日常工作。1. 先从选型说起MongoDB 的文档模型解决了什么问题1.1 和 MySQL 建表思维最不一样的地方如果之前只写过 MySQL第一次碰 MongoDB 最容易卡在“要不要建表”这个环节上。关系型数据库要先定义表结构每一行必须符合列约束字段变动基本离不开 ALTER TABLEMongoDB 里一个集合collection更像是容器往里面放的是 BSON 格式的文档字段可以随业务增减。比如一个用户集合早期数据可能就是{ _id: ObjectId(65b0a1c2e4b0f1a2b3c4d5e6), name: 张三, age: 30, tags: [技术, 摄影] }后面业务要加地址直接写入address字段就行不需要先改表。这是文档模型最舒服的地方也是最大的隐患字段太自由脏数据和格式漂移一样会悄悄出现。所以我的习惯是文档模型可以不用严格建表但应用层校验一定要有比如维护一份 JSON Schema或者在做复杂查询前先统一字段命名。对比着看会更清楚MySQL 关心的是“表、行、列、主外键、关系”MongoDB 关心的是“库、集合、文档、嵌套结构、索引”。这两种思维方式没有绝对好坏关键看你手头的数据是强结构、强一致还是变化快、嵌套深。1.2 适合直接上 MongoDB 的场景和该避开的场景很多人问我“能不能用 MongoDB 替代 MySQL”我一般不会直接回答能或不能而是列一张场景对照表更适合 MongoDB不建议硬上 MongoDB用户画像、标签系统字段经常变强事务、金额账务系统日志、埋点、物联网时序数据复杂多表 JOIN 报表内容管理、商品属性不固定需要窗口函数、复杂聚合分析快速原型、早期 MVP 迭代已有成熟 SQL 生态的团队数据仓库注意一点MongoDB 从 4.0 开始支持多文档事务但我不建议把它当成 MySQL 来用。日常写个跨集合事务还行高并发下事务的代价和锁等待一定要做压测。选型这件事我的判断标准是“这个数据是不是天生带文档结构以及改动频率高不高”。如果答案都是肯定直接上 MongoDB如果核心诉求是账务一致性和复杂关联老老实实留在关系型数据库。2. 环境准备装库、起服务、连客户端的那些坑2.1 安装、启动服务与基础配置检查本地体验 MongoDB 其实比想象中简单。多数 Linux 发行版都有官方源提供 mongodb-org 相关包安装后立刻把它当成服务跑起来systemctl start mongod systemctl status mongod有些容器里没有 systemd我会直接手动启动mongodmongod --dbpath /data/mongo --logpath /var/log/mongodb/mongod.log --fork手动启动有两点容易踩坑一是dbpath指向的目录必须先创建好并且有读写权限否则会报“Cannot create directory”之类的错二是--fork需要配合--logpath使用不然进程无法后台化。生产环境我建议用配置文件mongod.conf统一管理重点检查net.port、storage.dbPath、net.bindIp三项。bindIp这个参数很关键。默认很多发行版的配置只允许本机连接127.0.0.1如果你想让其他机器通过内网访问得在这里加上实际的服务器内网 IP。一旦开了外网端口必须配合认证和防火墙不要裸奔在公网上。2.2 连接串、认证参数与常用客户端工具连数据库首选官方提供的 mongosh旧版的 mongo shell 在新版里已经逐渐被替代。最简单的本地连接mongosh mongodb://127.0.0.1:27017/myblog如果开启了认证连接串会长一些mongosh mongodb://myuser:secret127.0.0.1:27017/myblog?authSourceadmin这里最容易忽略的是authSource。它表示“验证用户身份的库”默认情况下如果连接串里没写就用当前连接的库来做认证。你创建一个用户时如果指定的是admin库但登录时直接用业务库路径通常就会报认证失败。养成习惯凡是单机自建实例连接串都显式写上authSourceadmin。除了交互式 shell我还会用到两条基础命令mongodump用于逻辑备份mongorestore用于恢复。它们不同于文件系统快照但日常“遛一把”足够救急。图形客户端方面官方提供的 GUI 工具适合不熟悉命令行的同学看数据但我个人还是更推荐 shell因为脚本批量改数据时命令行效率要高一截。3. 数据库与集合层级的增删改查3.1 库和集合的创建、切换与删除MongoDB 里没有CREATE DATABASE这种显式建库命令库的“诞生”往往是被某个写操作触发的。进入 mongosh 后执行use myblog此时只是切换上下文什么都没创建。真正的动作发生在你往里面写第一条数据时db.users.insertOne({ name: 李四, age: 25 });这条命令同时触发了myblog库和users集合的创建。如果想让集合一开始就带参数比如使用固定大小集合可以用db.createCollection但日常业务里不常见。查看当前有哪些库和集合show dbs show collections删除动作一定要谨慎。删除集合用db.users.drop()整库删除用db.dropDatabase()。这里我不太赞成新手在测试库上随便敲db.dropDatabase()因为真实环境里误删之后恢复成本往往不小。我的经验是除非确认要清空全部数据否则先db.collection.find()看一眼集合里的数据量再决定是不是真要 drop。3.2 插入文档的三种方式与 write concern 概念插入操作主要有insertOne、insertMany以及旧版遗留的insert。从 MongoDB 5.0 起建议明确区分单条和多条db.users.insertOne({ name: 王五, age: 30 }); db.users.insertMany([ { name: 赵六, age: 22 }, { name: 孙七, age: 28 } ], { ordered: false });ordered: false是很多人没注意到的参数。默认ordered: true时如果批量数据中间某一条触发重复键错误后面的数据就不会再写入改成falseMongoDB 会跳过错误项继续写剩下的数据非常适合日志灌库这类场景。每条文档都有一个_id字段不指定时系统会自动生成ObjectId。这个字段全局唯一可以用于排序、分页和去重但要注意一旦你手动指定了重复的_id插入就会立刻报 E11000 duplicate key error。这里再提一下writeConcern。它控制写入请求要等到什么程度才返回成功db.users.insertOne( { name: 周八, age: 19 }, { writeConcern: { w: majority } } );单机模式下w: 1表示主节点确认即可在副本集里w: majority表示要等大多数节点写入后才返回结果。对一般业务来说默认配置足够但金融类或者数据不能丢的场景建议调高 writeConcern。不过每调高一级写入延迟也会相应增加这个平衡点要靠压测找。4. 查询操作find 的过滤、投影与操作符4.1 比较、逻辑、元素查询操作符查询是 MongoDB 使用频率最高的部分核心就是find()。一个最简单的过滤条件db.users.find({ age: { $gte: 18, $lt: 35 } });表示查年龄在 18 到 34 之间的用户。如果把$gte和$lt换成更具体的操作符日常覆盖范围会更广操作符作用示例$ne不等于{ status: { $ne: deleted } }$in在指定列表内{ status: { $in: [active, pending] } }$nin不在列表内{ status: { $nin: [banned] } }$exists判断字段是否存在{ address: { $exists: true } }$type按 BSON 类型过滤{ age: { $type: int } }逻辑条件也很直观。多个字段同时满足时MongoDB 隐式按 AND 处理db.users.find({ status: active, age: { $gte: 18 } });如果是显式的 AND 或 OR可以这样写db.users.find({ $and: [ { age: { $gte: 18 } }, { tags: 技术 } ] }); db.users.find({ $or: [ { status: active }, { vipLevel: { $gte: 3 } } ] });我的建议是不要一上来就堆复杂的$orMongoDB 对这种查询走索引并不像 MySQL 那么直观。如果条件里有多个字段先考虑建复合索引再写查询。4.2 排序、分页、limit/skip 与游标注意事项分页是基本操作里最常见的需求。按创建时间倒序取第一页db.users.find() .sort({ createdAt: -1 }) .skip(0) .limit(20);第一页很轻松但当你翻到第 100 页时skip(1980)可能已经把前面所有文档都扫描了一遍。这个问题的本质是 skip 是“跳过”而不是“跳到”每次都要从头数过去。数据量小无所谓数据量一大就是灾难。更推荐的做法是用_id或某个排序字段做游标分页db.users.find({ _id: { $gt: lastId } }) .sort({ _id: 1 }) .limit(20);这种方式不需要扫描被跳过的记录性能稳定得多。代价是没法像传统分页那样随意跳页但对无限滚动、信息流场景非常合适。查询结果默认返回所有字段如果只需要部分字段可以用投影db.users.find({ status: active }, { name: 1, age: 1, _id: 0 });投影里1表示要该字段0表示排除_id默认返回明确设置_id: 0才能去掉。还有一个容易踩的坑shell 中 find 返回的是游标数据很多时不会一次性全部加载到内存。交互模式下可以用it继续迭代脚本里如果确定数据量不大可以用toArray()或forEach()取出结果。5. 更新与删除从整行替换到局部修改5.1 updateOne/updateMany 与常见更新操作符更新文档是基本操作里最容易写错的一块核心在于弄清楚“替换”和“修改”的区别。下面这条是替换db.users.replaceOne( { _id: ObjectId(65b0a1c2e4b0f1a2b3c4d5e6) }, { name: 新名字, age: 99 } );替换后文档里只剩_id、name、age之前的tags、address都没了。如果只想改某个字段用更新操作符db.users.updateOne( { _id: ObjectId(65b0a1c2e4b0f1a2b3c4d5e6) }, { $set: { nickname: 阿新 }, $inc: { loginCount: 1 } } );我把日常高频操作符列一下操作符作用常见用法$set设置或更新字段{ $set: { status: active } }$unset删除字段{ $unset: { oldField: } }$inc数值增减{ $inc: { score: 10 } }$push数组追加元素{ $push: { tags: 生活 } }$pull从数组移除匹配元素{ $pull: { tags: 广告 } }$addToSet数组不存在才追加{ $addToSet: { tags: 技术 } }upsert是一个容易被忽略但很实用的选项。比如登录更新用户信息希望记录不存在时自动创建db.users.updateOne( { openid: u12345 }, { $set: { lastLoginAt: new Date() }, $inc: { loginCount: 1 } }, { upsert: true } );注意 MongoDB 4.0 之前的 shell 里update函数默认只更新一条要更新多条得带{ multi: true }很多老项目踩过这个坑。现在更明确的方式是直接分updateOne和updateMany语义不会产生歧义这也是我强烈推荐新项目直接用这两个方法的原因。5.2 删除的两种思路与误删恢复心态删除比更新更需要谨慎。最简单的方式是按条件删db.users.deleteOne({ _id: ObjectId(65b0a1c2e4b0f1a2b3c4d5e6) }); db.users.deleteMany({ status: inactive });我看到不少人清理无关数据时直接写db.users.deleteMany({})一旦条件没限制好整张表就直接清空了。所以我的习惯是批量删除前先跑一条等价的find().countDocuments()看看影响范围如果目标是清空整个集合用db.users.drop()比deleteMany({})快很多但集合上的索引也会一起被丢掉需要重新创建。线上业务数据我一般不建议物理删除更稳妥的方案是软删除打个标记db.users.updateOne( { _id: ObjectId(...) }, { $set: { deletedAt: new Date(), status: deleted } } );这样查询时多一个deletedAt: { $exists: false }条件数据还在心里不慌。MongoDB 删除后不像 MySQL 还有闪回这种机制所以执行任何 drop、deleteMany 之前最可靠的动作就是先备份。低成本的方式是mongodump --gzip导出一次再继续操作。这个习惯我坚持了很多年救过不止一次场。6. 索引设计对基本操作性能的影响6.1 单字段索引与复合索引的基本姿势基本操作做到后面性能瓶颈大部分出在索引上。MongoDB 不建索引也能查但数据一多 COLLSCAN 就会拖垮一切。先建一个最常用的单字段索引db.users.createIndex({ email: 1 });{ email: 1 }表示按 email 升序建索引-1 则是降序。对单字段查询来说方向不敏感但复合索引就要考虑顺序了db.users.createIndex({ status: 1, createdAt: -1 });这条复合索引对find({ status: active }).sort({ createdAt: -1 })很有用。设计复合索引最简单的心法是“等值字段放前面排序字段放后面范围查询尽量放最后”。如果查询条件里同时有等值和范围顺序放反了索引效果会大打折扣。索引还包括唯一索引用来保证某个字段不重复db.users.createIndex({ openid: 1 }, { unique: true });唯一索引能兜底很多业务幂等问题但要注意它也会让插入变慢。还有一个很实用的是 TTL 索引专门用于“过期自动清理”比如会话数据db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 });这条命令会让 createdAt 超过 1 小时的数据被后台自动删除比定时任务去 delete 省事得多。6.2 用 explain 看查询走了什么索引我判断一条查询到底快不快很少靠猜都是用 explain 看执行计划db.users.find({ status: active, age: { $gte: 18 } }) .sort({ createdAt: -1 }) .explain(executionStats);重点看两个地方winningPlan.stage和executionStats里的totalDocsExamined。winningPlan.stage如果显示COLLSCAN表示在扫描整个集合数据量大就会慢如果显示IXSCAN和FETCH说明走了索引再回表查文档这是比较健康的路径。totalDocsExamined和nReturned这两个数字也很关键。理想的查询是扫描的文档数尽量等于返回的文档数如果前者远大于后者说明索引选择性不够好。比如索引建在status上而大部分文档的status都是active查询结果是全表一半的数据优化器可能也懒得走索引。这时就要考虑把选择性更高的字段也放进复合索引。做 explain 是一劳永逸的技能。调索引前先看执行计划改完再看一遍对比比拍脑袋加十个索引有用得多。索引不是越多越好一个表上几十个索引会让写入明显变慢得不偿失。7. 常见问题与排查技巧实录7.1 几个必看的报错和排查思路我自己平时接到同学最多的求助基本都集中在这几个报错上整理成一个排查表报错或现象常见原因处理方式not authorized on db to execute command认证库不对或用户权限不足确认连接串的 authSourcegrant 对应角色ECONNREFUSEDmongod 没启动或 bindIp 不允许该地址访问查看服务状态检查配置文件 bindIpE11000 duplicate key唯一索引冲突或 _id 重复查一下冲突字段去除重复数据MongoNetworkTimeout网络延迟或防火墙拦截telnet 测试端口检查安全组Failed to parse查询条件类型错误比如字符串写成数字检查 BSON 类型确认字段真实类型其中“not authorized”最常见多半不是权限真的不够而是连接的库不对。例如创建用户时用admin库登录时却写成业务库路径等同于拿错了通行证。另一个高频问题就是 E11000尤其用insertMany导入历史数据时容易因为 _id 重复中断。处理批量导入我一般会先跑脚本筛掉重复项再用ordered: false兜底。7.2 一些文档里不会写但实测有用的习惯除了按部就班的命令最后分享几个我实际摸出来的操作习惯。第一交互式 mongosh 里 cursor 分批返回数据多时不需要一次全部打印多按it就能继续取下一批。但写脚本时要注意取完结果最好先判断数量避免无意识产生超大内存加载。第二统计文档数量用countDocuments()不要用已经被废弃的count()否则新版客户端会提示 deprecated。第三批量修改集合字段前先用find().limit(5)抽样看数据形态确认过滤条件不会误伤再跑updateMany。还有一点是关于导入导出。mongorestore恢复时如果目标库里已有相同 _id 的数据默认会报 duplicate key这时如果确实想覆盖旧数据可以加--drop参数先删集合再恢复。这个参数我是吃过亏才记住的不加它会卡在重复键上加了又确认好几次才敢执行。最后说一个我坚持了很久的动作凡是线上集合要做结构变更或者要跑大批量删除、更新我都会先mongodump --gzip打个备份再分批次小范围验证。MongoDB 基本操作确实不算难难的是每次操作前都多想一步数据是怎么存的、查询会走哪条索引、误操作怎么收场。把这些细节养成习惯MongoDB 用起来会顺手很多。

准备好开启您的数字化征程了吗?

专业团队为您量身定制,让您的企业网站脱颖而出

立即咨询