第 24 章 · MongoDB
一句话:文档数据库,一条记录是一份类似 JSON 的文档,同一个集合里的文档可以各有各的字段,查询和更新都按字段写条件。 用它还是用别的:数据天然是一份份嵌套的文档、字段经常变(日志、事件、商品属性、内容管理),用 MongoDB;要多表联查、跨多张表的强一致事务、报表 SQL,用 PostgreSQL(第 21 章)。 核对版本:MongoDB 8(随书 compose 镜像
mongo:8,目前指向 8.x 最新小版本)、mongosh 2.12、Database Tools 100.19,2026-10
24.1 安装与版本
Section titled “24.1 安装与版本”本书用 Docker 跑 MongoDB。在随书的 compose/ 目录下启动,Compose 的用法详见第 13 章:
docker compose --profile db up -d这一条同时起 PostgreSQL、MySQL、MongoDB。MongoDB 的宿主机端口是 27017,不设口令,数据在命名卷 mongodata 里。进容器里自带的 mongosh,出现 test> 提示符就是连上了,提示符里的 test 是当前库:
docker compose exec mongo mongosh镜像里同时带了 mongosh 和 Database Tools(mongodump、mongorestore、mongoimport 等),不在本机装也能做完本章的事。要在宿主机上用,分别装这两样;Ubuntu 上 mongosh 的装法见第 20 章 20.1,Database Tools 从官方下载页取 .deb:
winget install MongoDB.Shellwinget install MongoDB.DatabaseToolsbrew install mongoshbrew install mongodb/brew/mongodb-database-toolssudo apt install ./mongodb-database-tools-*.deb图形界面用官方的 MongoDB Compass:winget install MongoDB.Compass.Full,macOS 是 brew install --cask mongodb-compass。
查版本,前一条查本机 mongosh,后一条在 mongosh 里查服务端,返回 8. 开头的版本号:
mongosh --versiondb.version()不想用 Docker、要在本机安装服务的,见 24.7 的 MongoDB 手册里的安装一节。
24.2 五分钟概念
Section titled “24.2 五分钟概念”- 文档(Document)与集合(Collection):一条记录是一份文档,写法像 JSON,字段值可以是数组和嵌套的文档;存储时是二进制的 BSON,比 JSON 多了日期、
ObjectId、Decimal128等类型。文档放在集合里,集合放在库里,对应关系库的行、表、库。 _id:每份文档都有,集合里唯一。插入时不写,驱动自动生成一个ObjectId,里面带着生成时间;也可以自己给,比如用订单号。- 没有固定表结构:集合不用先建,第一次往里写就有了;同一个集合里的文档字段可以不同。字段名拼错不报错,只是多出一个新字段,要约束就给集合加校验规则(
$jsonSchema)。 - 索引(Index):和关系库一样是按某些字段排好序的目录,
_id自带一个。没有索引的查询要把集合里的文档一份份看完,执行计划里叫COLLSCAN,用上索引是IXSCAN。 - 聚合管道(Aggregation Pipeline):一组按顺序执行的阶段,每个阶段处理上一阶段输出的文档,
$match过滤、$group分组、$sort排序,相当于 SQL 的WHERE、GROUP BY、ORDER BY。下图是 24.4 里那条管道,五份订单进去,按城市汇总后剩两份:
24.3 常用命令
Section titled “24.3 常用命令”以下在 mongosh 提示符里输入,db 指当前库,db.orders 是当前库里的 orders 集合;最后两行是命令行程序,在容器里执行时前面加 docker compose exec mongo。通用的数据库概念和各客户端的对照见第 20 章。
| 任务 | 命令 | 说明 |
|---|---|---|
| 列出库 | show dbs |
没有数据的库不显示 |
| 换库 | use shop |
库不存在也能切过去,写入第一份文档时才建 |
| 列出集合 | show collections |
只列当前库的 |
| 插入一份 | db.orders.insertOne({ name: "Tom", city: "Beijing", amount: 59 }) |
返回 insertedId,即自动生成的 _id |
| 插入多份 | db.orders.insertMany([{ name: "Ann", amount: 120 }, { name: "Bob", amount: 50 }]) |
参数是数组 |
| 按条件查并只取几个字段 | db.orders.find({ amount: { $gte: 50 } }, { _id: 0, name: 1, amount: 1 }) |
第二个参数是投影(Projection),1 取、0 不取 |
| 改字段 | db.orders.updateOne({ name: "Tom" }, { $set: { status: "paid" } }) |
只改第一份匹配的;updateMany 改全部 |
| 删一份 | db.orders.deleteOne({ name: "Bob" }) |
deleteMany({}) 清空集合,不可恢复 |
| 计数 | db.orders.countDocuments({ city: "Beijing" }) |
不写条件数全部 |
| 建索引 | db.orders.createIndex({ city: 1 }) |
1 升序、-1 降序;返回索引名 city_1 |
| 列出索引 | db.orders.getIndexes() |
至少有一个 _id_ |
| 看执行计划 | db.orders.find({ city: "Beijing" }).explain("executionStats") |
看 stage 是 COLLSCAN 还是 IXSCAN,totalDocsExamined 是看过几份 |
| 聚合 | db.orders.aggregate([{ $group: { _id: "$city", total: { $sum: "$amount" } } }]) |
按城市求金额合计,"$amount" 指字段值 |
| 备份一个库 | mongodump --db=shop --archive=/tmp/shop.archive --gzip |
不写 --archive 时导出到当前目录下的 dump/ |
| 恢复 | mongorestore --archive=/tmp/shop.archive --gzip |
默认恢复回原库名,同 _id 的文档跳过不覆盖 |
24.4 典型工作流
Section titled “24.4 典型工作流”插入并查询一组订单
Section titled “插入并查询一组订单”- 进 mongosh:
docker compose exec mongo mongosh。 - 切到
shop库:use shop。 - 插入五份订单,返回五个
insertedIds:db.orders.insertMany([{ name: "Tom", city: "Beijing", amount: 59, tags: ["book"] }, { name: "Ann", city: "Shanghai", amount: 120 }, { name: "Bob", city: "Beijing", amount: 50 }, { name: "Lily", city: "Guangzhou", amount: 35 }, { name: "Jack", city: "Beijing", amount: 88, tags: ["food", "gift"] }])。 - 查北京的订单,只要名字和金额,得到三份:
db.orders.find({ city: "Beijing" }, { _id: 0, name: 1, amount: 1 })。 - 查带
gift标签的,条件写一个值就能匹配数组里的元素,得到 Jack:db.orders.find({ tags: "gift" })。 - 给 Tom 的订单加一个新字段,返回
modifiedCount: 1:db.orders.updateOne({ name: "Tom" }, { $set: { status: "paid" } })。 - 按城市汇总金额不小于 50 的订单,得到 Beijing 197、Shanghai 120,即 24.2 的图:
db.orders.aggregate([{ $match: { amount: { $gte: 50 } } }, { $group: { _id: "$city", total: { $sum: "$amount" }, n: { $sum: 1 } } }, { $sort: { total: -1 } }])。
给常用查询加索引并用 explain 确认
Section titled “给常用查询加索引并用 explain 确认”- 在
shop库里生成十万份事件,userId从 0 到 999 循环:db.events.insertMany(Array.from({ length: 100000 }, (_, i) => ({ userId: i % 1000, type: i % 2 ? "click" : "view" })))。 - 查一个用户的事件,看过的文档数是
100000,执行计划里是COLLSCAN:db.events.find({ userId: 42 }).explain("executionStats").executionStats.totalDocsExamined。 - 给查询条件里的字段建索引,返回
userId_1:db.events.createIndex({ userId: 1 })。 - 再看一次,看过的文档数降到
100,和返回的条数相同:db.events.find({ userId: 42 }).explain("executionStats").executionStats.totalDocsExamined。 - 看完整的执行计划,
winningPlan里出现IXSCAN和indexName: 'userId_1':db.events.find({ userId: 42 }).explain("executionStats")。 - 确认索引在列表里:
db.events.getIndexes()。
条件里同时有等值和排序的,如 find({ userId: 42 }).sort({ at: -1 }),建复合索引 { userId: 1, at: -1 },等值字段放前面。
在容器里备份并恢复一个库
Section titled “在容器里备份并恢复一个库”下面的命令在随书 compose/ 目录下执行,备份文件先放在容器里,再复制出来:
- 把
shop库导出成一个压缩的归档文件:docker compose exec mongo mongodump --db=shop --archive=/tmp/shop.archive --gzip。 - 复制到宿主机,文件名带日期:
docker compose cp mongo:/tmp/shop.archive ./shop-2026-10-08.archive。 - 恢复演练,恢复成另一个库
shop_restore,不碰原库:docker compose exec mongo mongorestore --archive=/tmp/shop.archive --gzip --nsFrom="shop.*" --nsTo="shop_restore.*"。 - 核对文档数和原库一致,输出
5:docker compose exec mongo mongosh shop_restore --quiet --eval "db.orders.countDocuments()"。 - 演练完删掉:
docker compose exec mongo mongosh shop_restore --quiet --eval "db.dropDatabase()"。
mongodump 导出的是逻辑备份,索引的定义也在里面,恢复时重建。定期备份、异地保存和恢复演练的做法见第 20 章 20.4。
24.5 配置要点
Section titled “24.5 配置要点”服务端程序叫 mongod,配置文件是 YAML 格式,缩进只能用空格。位置:
| 配置项 | 路径 | 说明 |
|---|---|---|
| 配置文件(Linux 安装包) | /etc/mongod.conf |
改完 sudo systemctl restart mongod |
| 配置文件(Windows 安装包) | bin\mongod.cfg |
在安装目录下,改完重启 MongoDB 服务 |
| 配置文件(Homebrew) | /opt/homebrew/etc/mongod.conf |
Intel 芯片的 Mac 在 /usr/local/etc/mongod.conf |
| Docker 镜像内 | 没有配置文件 | 用启动参数,写在 compose 的 command: 里;要用文件就挂载进去,再加 --config 指向它 |
最值得改的两项都关乎安全:
| 配置项 | 建议值 | 效果 |
|---|---|---|
security.authorization |
enabled |
开启认证,不登录不能读写;默认关闭,谁连上都能删库 |
net.bindIp |
127.0.0.1,10.0.0.5 |
只在列出的地址上监听,其余地址连不上;安装包默认只有 127.0.0.1,官方镜像默认监听全部地址 |
net.port |
27017 |
监听端口 |
storage.dbPath |
/var/lib/mongodb |
数据目录;Linux 安装包默认值,直接运行 mongod 时是 /data/db |
开认证要先有用户,否则开完谁都登录不了。先不开认证,在 mongosh 里 use admin,执行 db.createUser({ user: "admin", pwd: passwordPrompt(), roles: ["root"] }) 建管理员,再改配置并重启,之后用 mongosh "mongodb://admin@localhost:27017/?authSource=admin" 连接。随书镜像要开认证,在 compose 的 mongo 下加环境变量 MONGO_INITDB_ROOT_USERNAME、MONGO_INITDB_ROOT_PASSWORD,镜像会建好用户并开认证,但只在卷为空的首次启动时生效。
mongod.conf 的最小片段,# 后面是注释:
storage: dbPath: /var/lib/mongodb # 数据目录systemLog: destination: file # 日志写文件 path: /var/log/mongodb/mongod.log # 日志文件位置 logAppend: true # 重启后接着写,不覆盖net: port: 27017 # 监听端口 bindIp: 127.0.0.1,10.0.0.5 # 本机和内网地址,不写 0.0.0.0security: authorization: enabled # 开启认证,先建好管理员再打开24.6 注意事项
Section titled “24.6 注意事项”MongoServerError[Unauthorized]: Command listDatabases requires authentication。 原因:开了认证,连接没带用户或authSource不对。处理:mongosh "mongodb://admin@localhost:27017/?authSource=admin",admin 为建用户的库。- 查询很慢,
explain("executionStats")里是COLLSCAN,totalDocsExamined接近集合总数。 原因:查询条件的字段没有索引,要看遍整个集合。处理:db.events.createIndex({ userId: 1 }),再 explain 确认变成IXSCAN,见 24.4。 - 更新后文档只剩改的那个字段,其余字段全没了。 原因:用了
replaceOne或旧的update(),第二个参数没写$set,整份文档被替换;updateOne则报Update document requires atomic operators。处理:只改字段一律写{ $set: { 字段: 值 } }。 - 同一字段有的是
int、有的是double,强类型语言的程序读出来报类型转换错误。 原因:mongosh 把整数存成Int32,小数和别的程序写入的是Double。处理:写入时用Int32()、Long()指定类型;金额用Decimal128("9.99"),Double相加会算出0.30000000000000004。 - 数据被清空,只剩一个名为
READ_ME_TO_RECOVER_YOUR_DATA的库。 原因:MongoDB 监听在公网地址上又没开认证,被扫描程序删库勒索。处理:按 24.5 开authorization、收紧bindIp;compose 的端口保留127.0.0.1:前缀;用备份恢复。
24.7 深入阅读
Section titled “24.7 深入阅读”- MongoDB 手册:官方手册,CRUD、索引、聚合、安全、复制与分片都在这里,含各平台安装社区版服务端的步骤。
- mongosh 文档:mongosh 的连接写法、命令行参数、数据类型(Int32、Double、Decimal128)和脚本执行。
- MongoDB Database Tools:mongodump、mongorestore、mongoimport、mongoexport 的全部参数和安装方法。
- 聚合操作:聚合管道的工作方式、各个阶段($match、$group、$lookup 等)和与 SQL 的对照。
- 安全检查清单:上线前要做的安全设置:开启认证、最小权限、限制监听地址、加密连接。