1. 为什么命令行才是 MongoDB 的真正主战场
很多人第一次接触 MongoDB 都是从图形化工具开始的,点几下鼠标就能建库建表、插入数据,看起来确实方便。但真到了生产环境,尤其是需要批量处理数据、排查线上问题、写自动化脚本的时候,图形界面反而成了累赘。我见过太多人,本地开发用图形工具玩得飞起,一上服务器就懵了,因为服务器上根本没有图形界面,只有黑乎乎的命令行。
MongoDB 的命令行工具mongosh(旧版本叫mongo)才是真正贯穿开发、测试、运维全流程的核心武器。它不只是一个连接数据库的客户端,更是一个完整的 JavaScript 运行环境,你可以直接在里面对数据库做任何操作,还能写循环、定义函数、加载外部脚本。这篇文章就是把我这些年用 MongoDB 命令行踩过的坑、总结出来的高频操作、以及那些文档里不会明说的细节,一次性讲清楚。
不管你是刚接触 MongoDB 的新手,还是已经用了一段时间但总觉得命令行不够顺手的老手,下面这些内容都能让你直接抄作业。我会从最基础的连接开始,一路讲到聚合管道、索引管理、性能排查这些进阶操作,每个命令都配上实际场景和参数解释,保证你看完就能用。
2. 连接与基础环境操作
2.1 连接数据库的几种姿势
连接 MongoDB 的命令行客户端,最基础的写法是直接输入mongosh,它会默认连接到本地的mongodb://127.0.0.1:27017。但实际工作中,你面对的往往是远程服务器、副本集或者带认证的实例,这时候就需要指定完整的连接字符串。
# 连接本地默认实例 mongosh # 连接指定主机和端口 mongosh "mongodb://192.168.1.100:27017" # 带用户名密码认证的连接 mongosh "mongodb://admin:password123@192.168.1.100:27017/admin" # 连接副本集,指定副本集名称 mongosh "mongodb://192.168.1.100:27017,192.168.1.101:27017/mydb?replicaSet=rs0"这里有个细节很多人会搞混:认证数据库和业务数据库是两回事。上面第三个例子里,/admin表示用 admin 库里的用户做认证,但认证成功后你默认进入的还是 admin 库。如果你想直接进入业务库,需要在连接字符串里把业务库名字写在认证信息后面,或者连上之后手动use切换。
注意:密码里如果包含
@、:、/这些特殊字符,必须做 URL 编码,否则连接字符串会被解析错。比如密码是p@ss:word,要写成p%40ss%3Aword。这个坑我在实际项目里见过好几次,排查半天才发现是密码编码问题。
连接成功之后,你会看到一个test>或者>的提示符,说明已经进入了交互式命令行环境。这个环境支持 JavaScript 语法,你可以按上下箭头翻历史命令,按 Tab 键自动补全,体验和普通 shell 差不多。
2.2 数据库与集合的日常操作
进入命令行后,第一件事通常是看看当前有哪些数据库,然后切换到目标库。这些操作看似简单,但有几个容易忽略的点。
// 查看所有数据库 show dbs // 切换到指定数据库(不存在则创建) use myapp // 查看当前所在数据库 db // 查看当前库下所有集合 show collections // 删除当前数据库 db.dropDatabase() // 删除指定集合 db.users.drop()show dbs只会列出有数据的数据库,如果你刚use了一个新库但还没插入任何数据,它是不会出现在列表里的。这是新手经常困惑的地方,以为自己的库没建成功,其实只是空的库不显示而已。
创建集合有两种方式:一种是直接往一个不存在的集合里插入数据,MongoDB 会自动创建;另一种是用db.createCollection()显式创建,这种方式可以指定一些参数,比如是否是固定集合、最大大小等。
// 隐式创建,插入数据时自动建集合 db.articles.insertOne({ title: "测试文章", createdAt: new Date() }) // 显式创建,指定为固定集合,最大 10MB,最多 5000 条文档 db.createCollection("logs", { capped: true, size: 10485760, max: 5000 })固定集合(capped collection)这个特性值得单独说一下。它的特点是写入速度极快,而且当空间满了之后会自动覆盖最老的文档,非常适合做日志、监控数据这类只需要保留最近一段时间的场景。但它的缺点也很明显:文档不能删除,不能修改大小,更新操作也受限。所以用之前要想清楚数据模型是否匹配。
3. 文档增删改查的核心命令
3.1 插入数据的三种方式与选择
MongoDB 插入文档有三个命令:insertOne、insertMany和insert。前两个是推荐用法,语义清晰;insert是旧版兼容写法,可以插一条也可以插多条,但不推荐在新项目里用。
// 插入单条文档 db.users.insertOne({ name: "张三", age: 28, email: "zhangsan@example.com", tags: ["技术", "阅读"], createdAt: new Date() }) // 批量插入多条文档 db.users.insertMany([ { name: "李四", age: 32, email: "lisi@example.com" }, { name: "王五", age: 25, email: "wangwu@example.com" }, { name: "赵六", age: 40, email: "zhaoliu@example.com" } ])批量插入的时候有个参数叫ordered,默认是true,表示按顺序插入,一旦中间某条出错,后面的就全部停止。如果你希望即使有失败的也继续插入剩下的,可以设置ordered: false。
db.users.insertMany( [ { _id: 1, name: "A" }, { _id: 1, name: "B" }, // _id 重复,会报错 { _id: 2, name: "C" } ], { ordered: false } )上面这个例子,如果ordered: true,只有第一条会插入成功;如果ordered: false,第一条和第三条都会插入成功,第二条报错但不影响其他。这个细节在做数据迁移或者批量导入的时候特别有用,能避免因为个别脏数据导致整个批次失败。
实操心得:批量插入的数据量很大时,比如几万条,不要一次性全塞进
insertMany,内存会爆。建议分批,每批 1000 到 5000 条,用循环处理。我一般用 2000 条一批,兼顾效率和内存占用。
3.2 查询条件的写法与组合技巧
查询是日常用得最多的操作,find命令的基本用法很简单,但条件组合的写法有很多门道。
// 查询所有文档 db.users.find() // 等值查询 db.users.find({ name: "张三" }) // 多条件 AND db.users.find({ name: "张三", age: 28 }) // 条件查询:年龄大于 25 db.users.find({ age: { $gt: 25 } }) // 条件查询:年龄在 25 到 35 之间 db.users.find({ age: { $gte: 25, $lte: 35 } }) // 包含查询:tags 数组里包含"技术" db.users.find({ tags: "技术" }) // 数组内多条件:tags 同时包含"技术"和"阅读" db.users.find({ tags: { $all: ["技术", "阅读"] } }) // 或查询 db.users.find({ $or: [{ age: { $lt: 25 } }, { age: { $gt: 35 } }] }) // 字段存在性查询 db.users.find({ email: { $exists: true } }) // 正则查询:名字以"张"开头 db.users.find({ name: /^张/ })这里重点说一下数组查询。MongoDB 对数组的支持非常自然,{ tags: "技术" }这种写法会自动匹配数组中包含该元素的文档,不需要特殊的操作符。但如果要匹配数组中的多个元素,就必须用$all。还有一个容易混淆的是$elemMatch,它用于匹配数组中满足多个条件的同一个元素。
// 假设有文档:{ scores: [{ subject: "数学", score: 90 }, { subject: "语文", score: 85 }] } // 错误写法:会匹配到数学 90 或语文 85 的文档,但不要求是同一个元素 db.students.find({ "scores.subject": "数学", "scores.score": { $gt: 88 } }) // 正确写法:要求同一个元素同时满足两个条件 db.students.find({ scores: { $elemMatch: { subject: "数学", score: { $gt: 88 } } } })这个区别在实际业务里非常关键。比如你要查"数学成绩大于 88 分的学生",如果用第一种写法,可能会把"数学 80 分但语文 90 分"的学生也查出来,因为两个条件分别匹配了不同的数组元素。这种 bug 很隐蔽,测试的时候数据少不容易发现,上线后数据量大了才暴露出来。
3.3 更新操作的原子性与操作符
更新文档用updateOne、updateMany和replaceOne。新手最容易犯的错误是直接传一个对象进去,结果把整个文档替换掉了,而不是只更新指定字段。
// 错误示范:会把文档替换成只有 name 字段 db.users.updateOne({ name: "张三" }, { name: "张三丰" }) // 正确写法:用 $set 只更新指定字段 db.users.updateOne({ name: "张三" }, { $set: { name: "张三丰" } }) // 更新多条 db.users.updateMany({ age: { $lt: 30 } }, { $set: { status: "青年" } }) // 字段自增 db.users.updateOne({ name: "张三" }, { $inc: { age: 1 } }) // 数组追加元素 db.users.updateOne({ name: "张三" }, { $push: { tags: "运动" } }) // 数组去重追加 db.users.updateOne({ name: "张三" }, { $addToSet: { tags: "运动" } }) // 删除数组中的指定元素 db.users.updateOne({ name: "张三" }, { $pull: { tags: "运动" } }) // 删除字段 db.users.updateOne({ name: "张三" }, { $unset: { email: "" } })$addToSet和$push的区别值得强调:$push是无条件追加,哪怕元素已经存在也会再加一个;$addToSet会先检查,只有不存在时才添加。做标签系统的时候,用$addToSet能天然避免重复,省去了先查再判断的麻烦。
还有一个upsert参数,表示"如果不存在就插入"。这个在配置类数据、计数器场景下特别好用。
// 如果找到匹配文档就更新,找不到就插入一条新文档 db.counters.updateOne( { _id: "page_views" }, { $inc: { count: 1 } }, { upsert: true } )上面这个例子实现了一个页面访问计数器,每次调用都会把count加 1,如果计数器不存在就自动创建。这种写法比"先查再插或更新"要高效得多,而且天然是原子操作,不会出现并发问题。
3.4 删除操作的谨慎使用
删除命令有deleteOne和deleteMany,还有一个remove是旧版写法。删除操作不可逆,执行前一定要确认条件是否正确。
// 删除单条 db.users.deleteOne({ name: "张三" }) // 删除多条 db.users.deleteMany({ age: { $lt: 18 } }) // 删除集合内所有文档(保留集合本身和索引) db.users.deleteMany({}) // 删除集合(连同索引一起删除) db.users.drop()deleteMany({})和drop()的区别很多人搞不清楚。前者只删数据,集合的索引、分片配置都还在;后者把整个集合连根拔起,索引也没了。如果你只是想清空数据但保留表结构,用deleteMany({});如果确定这个集合不要了,用drop()更快,因为它不需要逐条删除。
注意:在生产环境执行删除操作前,先用同样的条件跑一次
find,确认查出来的文档就是你想删的。这个习惯帮我避免了好几次误删事故。另外,deleteMany如果条件写错了,比如{}写成了{ status: null },可能会删掉大量不该删的数据,所以条件一定要反复确认。
4. 索引管理与查询性能优化
4.1 索引的创建与查看
索引是 MongoDB 性能优化的第一手段,没有之一。一个没有索引的查询,在数据量大了之后就是灾难。创建索引的命令很简单,但索引的设计才是真正考验功力的地方。
// 创建单字段升序索引 db.users.createIndex({ name: 1 }) // 创建单字段降序索引 db.users.createIndex({ createdAt: -1 }) // 创建复合索引 db.users.createIndex({ age: 1, name: -1 }) // 创建唯一索引 db.users.createIndex({ email: 1 }, { unique: true }) // 创建稀疏索引(只索引存在该字段的文档) db.users.createIndex({ phone: 1 }, { sparse: true }) // 创建 TTL 索引,文档在指定时间后自动过期删除 db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 }) // 查看集合所有索引 db.users.getIndexes() // 查看索引大小 db.users.stats().indexSizes // 删除指定索引 db.users.dropIndex("name_1") // 删除除 _id 外的所有索引 db.users.dropIndexes()复合索引的字段顺序非常关键,它遵循"最左前缀原则"。比如你建了{ age: 1, name: -1 }这个索引,那么{ age: 1 }的查询能用上索引,但{ name: -1 }的查询用不上。所以设计复合索引时,要把最常用于筛选的字段放在前面。
TTL 索引是个很实用的特性,特别适合会话管理、临时验证码、日志清理这些场景。设置expireAfterSeconds后,MongoDB 会有一个后台线程定期扫描并删除过期文档。注意这个删除不是精确到秒的,通常有几十秒的延迟,所以对时效性要求极高的场景要留出余量。
4.2 用 explain 分析查询执行计划
光建索引还不够,你得知道查询到底有没有用上索引。explain命令就是干这个的。
// 查看查询执行计划 db.users.find({ age: { $gt: 25 } }).explain("executionStats")执行结果里重点看几个字段:
| 字段 | 含义 | 关注点 |
|---|---|---|
stage | 查询阶段 | COLLSCAN是全表扫描,IXSCAN是索引扫描 |
totalDocsExamined | 扫描的文档总数 | 越接近返回条数越好 |
totalKeysExamined | 扫描的索引键总数 | 越小越好 |
executionTimeMillis | 执行耗时(毫秒) | 结合数据量判断 |
nReturned | 返回的文档数 | 与扫描数对比 |
如果stage显示COLLSCAN,说明没走索引,数据量大了就会很慢。如果totalDocsExamined远大于nReturned,说明索引选择性不好,或者查询条件没有充分利用索引。
实操心得:我一般会对比
totalDocsExamined和nReturned的比值,如果超过 10:1,就说明索引效率有问题,需要优化。理想情况下这个比值应该接近 1:1,也就是扫描多少就返回多少。
4.3 索引使用的常见误区
索引不是越多越好,每个索引都会占用存储空间,而且会拖慢写入速度,因为每次插入、更新、删除都要维护所有相关索引。我见过一个集合建了十几个索引,写入性能惨不忍睹。
几个常见的索引误区:
- 给低选择性字段建索引:比如性别字段只有"男/女"两个值,建了索引还不如不建,因为扫描索引的开销可能比直接全表扫描还大。
- 复合索引字段顺序随意:前面说过的最左前缀原则,顺序错了索引就废了。
- 忽略索引覆盖:如果查询只需要返回索引里包含的字段,MongoDB 可以直接从索引返回结果,不用回表查文档,这叫"覆盖查询"。比如建了
{ name: 1, age: 1 }索引,查询db.users.find({ name: "张三" }, { name: 1, age: 1, _id: 0 })就能走覆盖查询。 - 在数组字段上建复合索引:数组字段的索引是"多键索引",一个文档会在索引里出现多次,复合索引里如果有多个数组字段,会导致索引条目爆炸式增长。
5. 聚合管道与数据处理实战
5.1 聚合管道的基本概念
聚合管道是 MongoDB 最强大的数据处理工具,它把一系列处理阶段串联起来,每个阶段的输出作为下一个阶段的输入,像流水线一样处理数据。理解聚合管道,关键是把每个阶段想象成一个"加工车间",数据从一头进去,经过一道道工序,从另一头出来就是成品。
db.orders.aggregate([ { $match: { status: "completed" } }, { $group: { _id: "$userId", total: { $sum: "$amount" } } }, { $sort: { total: -1 } }, { $limit: 10 } ])上面这个例子做了四件事:先筛选出已完成的订单,然后按用户分组求和,再按总金额降序排列,最后取前 10 名。如果用传统 SQL 写,大概是SELECT userId, SUM(amount) FROM orders WHERE status = 'completed' GROUP BY userId ORDER BY SUM(amount) DESC LIMIT 10。对比一下能看出来,聚合管道的写法更直观,数据流向一目了然。
5.2 常用聚合阶段详解
$match和$group是最常用的两个阶段。$match尽量放在管道最前面,因为它能减少后续阶段处理的数据量,而且如果$match用到了索引,还能直接利用索引加速。
// $group 常用累加器 db.sales.aggregate([ { $group: { _id: "$productId", totalSales: { $sum: "$amount" }, // 求和 avgPrice: { $avg: "$price" }, // 平均值 maxPrice: { $max: "$price" }, // 最大值 minPrice: { $min: "$price" }, // 最小值 count: { $sum: 1 }, // 计数 firstSale: { $first: "$date" }, // 分组内第一条 lastSale: { $last: "$date" } // 分组内最后一条 } } ])$project用于控制输出字段,可以重命名字段、计算新字段、排除字段。
db.users.aggregate([ { $project: { _id: 0, fullName: { $concat: ["$firstName", " ", "$lastName"] }, ageGroup: { $cond: { if: { $lt: ["$age", 30] }, then: "青年", else: "中年" } }, email: 1 } } ])$unwind用于展开数组,把一条包含数组的文档拆成多条文档,每条包含数组中的一个元素。这个阶段在处理标签、评论、订单明细这类嵌套数据时特别有用。
// 原文档:{ _id: 1, tags: ["A", "B", "C"] } db.articles.aggregate([ { $unwind: "$tags" } ]) // 结果:{ _id: 1, tags: "A" }, { _id: 1, tags: "B" }, { _id: 1, tags: "C" }$lookup实现类似 SQL 的左连接,可以在一个集合里关联查询另一个集合的数据。
db.orders.aggregate([ { $lookup: { from: "users", localField: "userId", foreignField: "_id", as: "userInfo" } }, { $unwind: "$userInfo" }, { $project: { "userInfo.name": 1, amount: 1 } } ])注意:
$lookup虽然方便,但性能开销比较大,尤其是关联的集合数据量很大时。如果频繁使用$lookup,可能说明数据模型设计有问题,需要考虑是否应该把关联数据内嵌到主文档里。MongoDB 的设计哲学是"文档内嵌优先,关联查询次之",这一点和关系型数据库的思维很不一样。
5.3 聚合性能优化技巧
聚合管道的性能优化有几个原则:
第一,$match和$limit尽量往前放。$match放前面能减少数据量,$limit放前面能减少后续处理。但要注意,如果$limit放在$sort前面,结果可能不是你想要的,因为排序还没做就截断了。
第二,$group之前尽量先用$match过滤。$group需要把所有数据加载到内存里做分组,数据量大了会触发内存限制。MongoDB 默认的聚合内存限制是 100MB,超过会报错。如果确实需要处理大量数据,可以开启allowDiskUse选项,让 MongoDB 把中间结果写到磁盘上。
db.orders.aggregate([ { $match: { status: "completed" } }, { $group: { _id: "$userId", total: { $sum: "$amount" } } } ], { allowDiskUse: true })第三,善用索引。$match、$sort这些阶段如果能用上索引,性能会有数量级的提升。可以用explain查看聚合的执行计划,确认是否走了索引。
6. 数据库状态监控与问题排查
6.1 查看数据库和集合状态
日常运维中,经常需要查看数据库的运行状态,比如数据大小、索引大小、连接数等。
// 查看数据库统计信息 db.stats() // 查看集合统计信息 db.users.stats() // 查看集合的存储大小 db.users.dataSize() // 查看当前数据库的连接数 db.serverStatus().connections // 查看当前正在执行的操作 db.currentOp() // 查看慢查询日志 db.getProfilingStatus()db.stats()返回的信息很丰富,包括集合数量、文档总数、数据大小、存储大小、索引数量等。其中dataSize是数据本身的逻辑大小,storageSize是实际占用的磁盘空间,后者通常比前者大,因为 MongoDB 会预留一些空间。indexSize是索引占用的空间,如果这个值很大,说明索引建多了,需要考虑精简。
6.2 慢查询分析与优化
MongoDB 有一个 profiling 功能,可以记录执行时间超过指定阈值的操作。默认情况下 profiling 是关闭的,需要手动开启。
// 开启 profiling,记录超过 100ms 的操作 db.setProfilingLevel(1, { slowms: 100 }) // 查看 profiling 状态 db.getProfilingStatus() // 查看最近的慢查询 db.system.profile.find().sort({ ts: -1 }).limit(10) // 关闭 profiling db.setProfilingLevel(0)profiling 级别有三个:0 表示关闭,1 表示只记录慢操作,2 表示记录所有操作。生产环境一般用级别 1,阈值设 100ms 到 200ms 之间。级别 2 会记录所有操作,对性能有影响,只在排查特定问题时临时开启。
查看慢查询日志时,重点看millis(执行耗时)、planSummary(执行计划)、docsExamined(扫描文档数)、nreturned(返回文档数)这几个字段。如果docsExamined远大于nreturned,说明索引效率低;如果planSummary是COLLSCAN,说明没走索引。
6.3 常见故障与排查思路
问题一:连接数满了
报错信息通常是too many connections。先用db.serverStatus().connections查看当前连接数和最大连接数。如果确实满了,要么调大最大连接数(通过启动参数maxIncomingConnections),要么排查是不是有连接泄漏。连接泄漏常见于应用程序没有正确关闭连接,或者连接池配置过大。
问题二:查询突然变慢
先看是不是数据量增长了导致全表扫描变慢,用explain确认执行计划。如果之前走索引现在不走了,可能是索引被删了,或者查询条件变了导致索引失效。还有一种可能是锁竞争,用db.currentOp()查看是否有长时间运行的操作阻塞了其他查询。
问题三:磁盘空间不足
用db.stats()查看数据大小和索引大小,判断是数据本身占满了还是索引占满了。如果是索引占满了,考虑删除不必要的索引。如果是数据占满了,考虑归档历史数据或者分片。另外,MongoDB 删除数据后不会立即释放磁盘空间,需要执行db.runCommand({ compact: "集合名" })来整理碎片,但这个操作会阻塞其他操作,要在业务低峰期执行。
问题四:副本集同步延迟
用rs.status()查看副本集状态,重点关注optimeDate和lastHeartbeat字段。如果从节点落后主节点太多,可能是网络问题,也可能是从节点性能不足。还有一种常见原因是主节点写入量太大,从节点来不及同步,这时候需要考虑增加从节点或者优化写入。
7. 数据导入导出与备份恢复
7.1 使用 mongoimport 和 mongoexport
mongoimport和mongoexport是处理 JSON、CSV 数据的利器,适合小规模数据迁移和备份。
# 导出集合为 JSON 文件 mongoexport --db=myapp --collection=users --out=users.json # 导出为 CSV 格式 mongoexport --db=myapp --collection=users --type=csv --fields=name,age,email --out=users.csv # 导入 JSON 文件 mongoimport --db=myapp --collection=users --file=users.json # 导入 CSV 文件 mongoimport --db=myapp --collection=users --type=csv --headerline --file=users.csv # 带认证的导出 mongoexport --uri="mongodb://admin:password@192.168.1.100:27017/myapp" --collection=users --out=users.json--headerline参数表示 CSV 文件的第一行是字段名,导入时会自动用这些字段名。如果不加这个参数,MongoDB 会用field1、field2这样的默认名,导入后还得手动改,很麻烦。
注意:
mongoexport导出的是文本格式,对于二进制数据(比如图片、文件)支持不好,而且导出过程中如果数据有变化,可能导出的不是一致性快照。生产环境的大规模备份应该用mongodump。
7.2 使用 mongodump 和 mongorestore
mongodump和mongorestore是 MongoDB 官方的备份恢复工具,导出的是 BSON 格式,保留了数据类型和索引信息,适合完整的数据库备份。
# 备份整个数据库 mongodump --db=myapp --out=/backup/ # 备份指定集合 mongodump --db=myapp --collection=users --out=/backup/ # 备份所有数据库 mongodump --out=/backup/ # 恢复数据库 mongorestore --db=myapp /backup/myapp/ # 恢复时删除原有数据 mongorestore --db=myapp --drop /backup/myapp/ # 从归档文件恢复 mongorestore --archive=/backup/myapp.archive --gzip--drop参数表示恢复前先删除现有集合,确保恢复后的数据和备份完全一致。如果不加这个参数,恢复操作是"合并"模式,已有的文档会被覆盖,但备份里没有的文档会保留,可能导致数据不一致。
--gzip参数可以在备份时压缩,大幅减少磁盘占用。对于文本数据为主的集合,压缩率通常能达到 70% 以上。备份命令加上--gzip,恢复时也要加,否则会报格式错误。
7.3 备份策略与注意事项
备份不是简单地跑个mongodump就完事了,有几个关键点需要考虑:
备份频率:根据数据重要性和变化频率决定。核心业务数据建议每天至少一次全量备份,加上实时的增量备份(通过 oplog)。非核心数据可以降低频率。
备份保留:一般保留最近 7 天的每日备份,加上最近 4 周的每周备份,加上最近 12 个月的每月备份。这个策略能覆盖大多数误删、数据损坏的场景。
备份验证:备份完一定要验证,最直接的方法是在测试环境恢复一次,确认数据完整可用。我见过太多"备份成功但恢复失败"的案例,问题往往出在备份文件损坏或者版本不兼容。
备份存储:不要和数据库放在同一台机器上,否则机器挂了备份也没了。建议放到独立的存储设备或者对象存储里。
实操心得:
mongodump在备份过程中会对数据库产生一定压力,尤其是数据量大的时候。建议在业务低峰期执行,并且加上--numParallelCollections参数控制并行度,避免把数据库压垮。另外,如果数据库开启了认证,备份命令也要带上认证信息,否则会连接失败。
8. 命令行效率提升技巧
8.1 自定义函数与脚本加载
mongosh支持 JavaScript,你可以定义自己的函数来简化重复操作。比如经常需要按某个条件统计文档数,可以定义一个辅助函数。
// 定义快捷查询函数 function findByName(name) { return db.users.find({ name: name }).toArray() } // 定义分页查询函数 function paginate(collection, query, page, pageSize) { return db[collection] .find(query) .skip((page - 1) * pageSize) .limit(pageSize) .toArray() } // 使用 findByName("张三") paginate("users", { age: { $gt: 25 } }, 1, 10)如果函数比较多,可以写到一个.js文件里,然后用load()加载。
// 加载外部脚本 load("/path/to/my_helpers.js")这个技巧在写复杂的数据处理脚本时特别有用,你可以把常用的查询、统计、数据转换逻辑封装成函数,需要的时候直接调用,不用每次都敲一遍。
8.2 批量操作与循环处理
命令行里可以直接写循环,批量处理数据。
// 批量更新:给所有年龄大于 30 的用户加标签 db.users.find({ age: { $gt: 30 } }).forEach(function(user) { db.users.updateOne( { _id: user._id }, { $addToSet: { tags: "资深" } } ) }) // 批量删除:删除 30 天前的日志 var cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) db.logs.deleteMany({ createdAt: { $lt: cutoff } }) // 分批处理大量数据 var batchSize = 1000 var processed = 0 var cursor = db.largeCollection.find({ status: "pending" }) while (cursor.hasNext()) { var doc = cursor.next() // 处理逻辑 db.largeCollection.updateOne( { _id: doc._id }, { $set: { status: "processed" } } ) processed++ if (processed % batchSize === 0) { print("已处理 " + processed + " 条") } } print("处理完成,共 " + processed + " 条")用forEach遍历游标时要注意,如果数据量很大,游标可能会超时。MongoDB 默认的游标超时时间是 10 分钟,如果处理时间超过这个限制,游标会被自动关闭。解决办法是设置noCursorTimeout选项,或者分批处理。
// 设置游标不超时 db.largeCollection.find({ status: "pending" }, { noCursorTimeout: true })注意:
noCursorTimeout要谨慎使用,如果程序异常退出,游标没有正确关闭,会一直占用服务器资源。建议配合try-finally确保游标最终被关闭。
8.3 输出格式化与结果处理
命令行默认的输出格式有时候不够直观,可以用pretty()让结果更易读。
// 格式化输出 db.users.find().pretty() // 只返回指定字段 db.users.find({}, { name: 1, age: 1, _id: 0 }) // 统计数量 db.users.countDocuments({ age: { $gt: 25 } }) // 去重统计 db.users.distinct("age") // 结果排序 db.users.find().sort({ age: -1 }).limit(5) // 结果转为数组 var arr = db.users.find().toArray() printjson(arr)countDocuments和estimatedDocumentCount的区别也值得说一下。countDocuments会精确统计,但需要扫描数据,慢;estimatedDocumentCount直接读集合的元数据,快但不精确。如果只是想知道大概数量,用后者;如果需要精确值,用前者。
9. 权限管理与安全操作
9.1 用户与角色管理
MongoDB 的用户是跟着数据库走的,创建用户时要在目标数据库下操作。
// 切换到 admin 库 use admin // 创建管理员用户 db.createUser({ user: "admin", pwd: "securePassword123", roles: [ { role: "userAdminAnyDatabase", db: "admin" }, { role: "readWriteAnyDatabase", db: "admin" } ] }) // 在业务库创建只读用户 use myapp db.createUser({ user: "readonly", pwd: "readonlyPass456", roles: [ { role: "read", db: "myapp" } ] }) // 查看所有用户 db.system.users.find() // 修改用户密码 db.changeUserPassword("readonly", "newPassword789") // 删除用户 db.dropUser("readonly") // 给用户添加角色 db.grantRolesToUser("readonly", [{ role: "readWrite", db: "myapp" }]) // 撤销用户角色 db.revokeRolesFromUser("readonly", [{ role: "readWrite", db: "myapp" }])MongoDB 内置了很多角色,常用的有read(只读)、readWrite(读写)、dbAdmin(数据库管理)、userAdmin(用户管理)、clusterAdmin(集群管理)。生产环境要遵循最小权限原则,应用程序连接数据库的账号只给必要的权限,不要图省事直接用 admin 账号。
9.2 安全配置检查清单
除了用户权限,还有几个安全相关的配置需要检查:
- 开启认证:启动参数加上
--auth,或者在配置文件里设置security.authorization: enabled。没有开启认证的 MongoDB 实例,任何人都能连接和操作,极其危险。 - 绑定 IP:默认 MongoDB 只监听 127.0.0.1,如果改成 0.0.0.0 监听所有网卡,一定要配合防火墙规则,只允许可信 IP 访问。
- 禁用 HTTP 接口:旧版本 MongoDB 有一个 HTTP 状态接口,默认在 28017 端口,会暴露一些数据库信息,建议禁用。
- 定期审计:定期检查用户列表和权限分配,删除不再使用的账号,收回多余的权限。
实操心得:我见过不少项目在开发阶段为了方便,直接裸奔不加认证,上线时忘了加,结果被扫描到导致数据泄露。建议从项目第一天就开启认证,开发环境也不例外,养成习惯就不会忘。
10. 个人实操体会与建议
说了这么多命令和技巧,最后分享几点我在实际工作中总结出来的体会。
第一,命令行的熟练度直接决定排查问题的速度。线上出问题的时候,往往没有时间让你慢慢查文档,能不能快速敲出正确的命令,决定了故障恢复的时间。建议把常用的命令整理成自己的速查表,定期复习。
第二,理解原理比记住命令更重要。MongoDB 的命令很多,但核心概念就那么几个:文档模型、索引、聚合管道、副本集、分片。理解了这些概念,遇到新命令也能快速推断出用法。
第三,生产环境的操作要谨慎再谨慎。删除、更新、修改索引这些操作,执行前一定要确认条件,最好先在测试环境验证。我个人的习惯是,任何可能影响多行数据的操作,都先用find跑一遍确认结果,然后再执行。
第四,善用explain和 profiling。这两个工具能帮你发现绝大多数性能问题。不要等到用户投诉了才去查,平时就养成定期检查慢查询的习惯。
第五,备份是最后的底线。不管你的技术多好,总有出错的时候,备份是唯一能让你在出错后还有退路的东西。备份策略要根据业务重要性来定,但无论如何,一定要有备份,而且要定期验证备份可用。
这些经验都是踩坑踩出来的,希望能帮你少走一些弯路。MongoDB 的命令行工具功能远不止我上面写的这些,还有很多高级特性比如变更流、事务、分片管理等等,等你有了一定基础之后可以继续深入。