跳转到内容

第 24 章 · MongoDB

一句话:文档数据库,一条记录是一份类似 JSON 的文档,同一个集合里的文档可以各有各的字段,查询和更新都按字段写条件。 用它还是用别的:数据天然是一份份嵌套的文档、字段经常变(日志、事件、商品属性、内容管理),用 MongoDB;要多表联查、跨多张表的强一致事务、报表 SQL,用 PostgreSQL(第 21 章)。 核对版本:MongoDB 8(随书 compose 镜像 mongo:8,目前指向 8.x 最新小版本)、mongosh 2.12、Database Tools 100.19,2026-10

本书用 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.Shell
winget install MongoDB.DatabaseTools
终端窗口
brew install mongosh
brew install mongodb/brew/mongodb-database-tools
sudo apt install ./mongodb-database-tools-*.deb

图形界面用官方的 MongoDB Compass:winget install MongoDB.Compass.Full,macOS 是 brew install --cask mongodb-compass。

查版本,前一条查本机 mongosh,后一条在 mongosh 里查服务端,返回 8. 开头的版本号:

终端窗口
mongosh --version
db.version()

不想用 Docker、要在本机安装服务的,见 24.7 的 MongoDB 手册里的安装一节。

  • 文档(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 里那条管道,五份订单进去,按城市汇总后剩两份:

orders 集合的 5 份文档先经 $match 留下金额不小于 50 的 4 份,再经 $group 按城市分组求和,最后 $sort 按总额排序,得到 Beijing 197、Shanghai 120 两份结果

以下在 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 的文档跳过不覆盖
  1. 进 mongosh:docker compose exec mongo mongosh。
  2. 切到 shop 库:use shop。
  3. 插入五份订单,返回五个 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"] }])。
  4. 查北京的订单,只要名字和金额,得到三份:db.orders.find({ city: "Beijing" }, { _id: 0, name: 1, amount: 1 })。
  5. 查带 gift 标签的,条件写一个值就能匹配数组里的元素,得到 Jack:db.orders.find({ tags: "gift" })。
  6. 给 Tom 的订单加一个新字段,返回 modifiedCount: 1:db.orders.updateOne({ name: "Tom" }, { $set: { status: "paid" } })。
  7. 按城市汇总金额不小于 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 确认”
  1. 在 shop 库里生成十万份事件,userId 从 0 到 999 循环:db.events.insertMany(Array.from({ length: 100000 }, (_, i) => ({ userId: i % 1000, type: i % 2 ? "click" : "view" })))。
  2. 查一个用户的事件,看过的文档数是 100000,执行计划里是 COLLSCAN:db.events.find({ userId: 42 }).explain("executionStats").executionStats.totalDocsExamined。
  3. 给查询条件里的字段建索引,返回 userId_1:db.events.createIndex({ userId: 1 })。
  4. 再看一次,看过的文档数降到 100,和返回的条数相同:db.events.find({ userId: 42 }).explain("executionStats").executionStats.totalDocsExamined。
  5. 看完整的执行计划,winningPlan 里出现 IXSCAN 和 indexName: 'userId_1':db.events.find({ userId: 42 }).explain("executionStats")。
  6. 确认索引在列表里:db.events.getIndexes()。

条件里同时有等值和排序的,如 find({ userId: 42 }).sort({ at: -1 }),建复合索引 { userId: 1, at: -1 },等值字段放前面。

下面的命令在随书 compose/ 目录下执行,备份文件先放在容器里,再复制出来:

  1. 把 shop 库导出成一个压缩的归档文件:docker compose exec mongo mongodump --db=shop --archive=/tmp/shop.archive --gzip。
  2. 复制到宿主机,文件名带日期:docker compose cp mongo:/tmp/shop.archive ./shop-2026-10-08.archive。
  3. 恢复演练,恢复成另一个库 shop_restore,不碰原库:docker compose exec mongo mongorestore --archive=/tmp/shop.archive --gzip --nsFrom="shop.*" --nsTo="shop_restore.*"。
  4. 核对文档数和原库一致,输出 5:docker compose exec mongo mongosh shop_restore --quiet --eval "db.orders.countDocuments()"。
  5. 演练完删掉:docker compose exec mongo mongosh shop_restore --quiet --eval "db.dropDatabase()"。

mongodump 导出的是逻辑备份,索引的定义也在里面,恢复时重建。定期备份、异地保存和恢复演练的做法见第 20 章 20.4。

服务端程序叫 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.0
security:
authorization: enabled # 开启认证,先建好管理员再打开
  • 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: 前缀;用备份恢复。
  • MongoDB 手册:官方手册,CRUD、索引、聚合、安全、复制与分片都在这里,含各平台安装社区版服务端的步骤。
  • mongosh 文档:mongosh 的连接写法、命令行参数、数据类型(Int32、Double、Decimal128)和脚本执行。
  • MongoDB Database Tools:mongodump、mongorestore、mongoimport、mongoexport 的全部参数和安装方法。
  • 聚合操作:聚合管道的工作方式、各个阶段($match、$group、$lookup 等)和与 SQL 的对照。
  • 安全检查清单:上线前要做的安全设置:开启认证、最小权限、限制监听地址、加密连接。