Skip to content

第八章 MongoDB 详解

📖 ⏱️ 预计阅读时长

1. NoSQL 与 MongoDB 概述

在传统关系型数据库占据主流地位的数十年间,随着互联网规模的爆发式增长,越来越多的场景对数据的灵活性、水平扩展能力和高吞吐提出了新的要求。NoSQL(Not Only SQL)数据库应运而生,MongoDB 是其中最具代表性的文档型数据库之一。理解 MongoDB 有助于拓展对数据存储范式的认知。

1.1 NoSQL 的兴起背景

关系型数据库以严格的模式(Schema)和 ACID 事务著称,但在以下场景中面临挑战:

  • 模式演进频繁:业务快速迭代时,表结构变更成本高。
  • 水平扩展困难:单机性能瓶颈难以通过简单加机器解决。
  • 高并发写入:关系型数据库的锁机制在极高并发下成为瓶颈。
  • 半结构化数据:JSON、嵌套对象等非表结构数据在关系模型中需要拆表或使用 JSON 列。

NoSQL 数据库通过放宽部分约束,换取更好的灵活性和扩展性。根据数据模型,NoSQL 可分为键值型(如 Redis)、文档型(如 MongoDB)、列族型(如 HBase)和图数据库(如 Neo4j)。

1.2 MongoDB 简介

MongoDB 由 10gen 公司(现 MongoDB Inc.)于 2009 年发布,是一个基于分布式文件存储的开源文档型数据库。其名称源自英文 "humongous"(巨大的),寓意处理海量数据。MongoDB 使用 BSON(Binary JSON)存储文档,支持丰富的查询和聚合能力,被广泛应用于内容管理、用户画像、日志分析、物联网等场景。

学习 MongoDB 时,最大的认知转换并不是把 SQL 换成一套新的命令,而是把数据组织方式从“以表为中心”切换为“以文档对象为中心”。关系模型通常先识别实体和联系,再通过规范化把事实拆分到不同表中;文档模型则更强调围绕一个业务对象,把经常一起读取的数据组织成一个整体文档。只要真正完成这种思维切换,后续关于嵌入、引用、聚合和分片的知识点才会连成体系。

1.3 MongoDB 核心概念对照

关系型数据库MongoDB
数据库 (Database)数据库 (Database)
表 (Table)集合 (Collection)
行 (Row)文档 (Document)
列 (Column)字段 (Field)
主键 (Primary Key)_id 字段(自动生成)
外键 (Foreign Key)嵌入文档或引用(由应用维护)

2. MongoDB 安装与连接

2.1 安装方式

  • Linux:按官方文档添加 MongoDB 源后,使用 aptyum 安装。
  • Windows:下载 MSI 安装包,按向导完成安装。
  • macOSbrew tap mongodb/brew && brew install mongodb-community
  • Dockerdocker run -d -p 27017:27017 mongo

2.2 启动服务

安装后需启动 mongod 服务。默认数据目录为 /data/db(Linux/macOS)或安装目录下的 data(Windows),监听端口 27017。

2.3 连接 MongoDB

使用 mongosh(MongoDB Shell,6.0+)或旧版 mongo 连接:

bash
mongosh                    # 连接本地
mongosh "mongodb://localhost:27017"
mongosh "mongodb://user:pass@host:27017/mydb"  # 带认证

连接成功后进入 test> 提示符。

MongoDB 的连接方式看似和其他数据库差不多,真正需要注意的是它常常天然处在分布式和多节点环境中。因此,连接串不仅仅是“怎么登录”的问题,还可能隐含认证方式、读偏好、副本集名称和目标数据库等配置。教材里先把单节点连接讲清楚,是为了让读者后续理解副本集和分片时,知道客户端其实一直在和一个可能会变化的集群入口打交道,而不只是固定的一台服务器。


3. 数据库与集合操作

3.1 创建与切换数据库

javascript
// 切换到数据库(不存在则创建)
use school

// 查看当前数据库
db

// 查看所有数据库
show dbs

MongoDB 的数据库在第一次插入数据后才真正创建,空数据库不会出现在 show dbs 中。

3.2 删除数据库

javascript
use school
db.dropDatabase()

3.3 集合操作

javascript
// 创建集合(可选,插入时自动创建)
db.createCollection("students", { capped: false })

// 查看集合
show collections

// 删除集合
db.students.drop()

固定集合 (Capped Collection):可指定 capped: truesize,用于日志等有大小上限的场景,写入超出后自动覆盖最旧数据。

集合与关系数据库中的表最相近,但它们的使用哲学并不相同。关系表通常要求结构先定义清楚,再稳定承载同类事实;MongoDB 集合虽然也应尽量保持结构稳定,却允许在一定范围内接受字段逐步演化。正因为存在这种灵活性,集合设计反而更需要提前思考命名、文档边界和版本演进方式。若把集合理解成“随便放文档的桶”,后期最容易出现的就是字段漂移和查询规则失控。


4. 文档与 BSON

4.1 BSON 简介

BSON(Binary JSON)是 JSON 的二进制扩展,支持更多数据类型,如 ObjectIdDateBinary 等。MongoDB 以 BSON 格式存储文档,每个文档最大 16MB。

4.2 文档结构

文档是键值对的集合,键为字符串,值可以是任意 BSON 类型,包括嵌套文档和数组:

javascript
{
    "_id": ObjectId("507f1f77bcf86cd799439011"),
    "sno": "2021001",
    "sname": "张三",
    "sage": 20,
    "sdept": "计算机系",
    "courses": ["C001", "C002"],
    "address": {
        "city": "北京",
        "street": "中关村大街1号"
    }
}

文档结构的优势,在于它天然适合表达层级关系和半结构化对象。例如一个内容对象既包含标题和正文,也包含标签、作者快照、扩展属性和多层嵌套配置,把这些信息作为一个整体文档存储,往往比拆成大量小表更符合读取习惯。但这并不意味着“什么都往一个文档里塞”就是好设计。若子结构会无限增长、经常独立更新,或者会被多处复用,那么过度嵌入反而会让文档过大、更新复杂、冗余失控。因此,MongoDB 的关键并不是放弃结构设计,而是把结构设计的重点转移到对象边界和读取路径上。

4.3 _id 字段

每个文档必须有 _id 字段,且在同一集合内唯一。若插入时不指定,MongoDB 自动生成 ObjectIdObjectId 为 12 字节,包含时间戳、机器标识、进程 ID 和计数器,可按时间大致排序。


5. 插入文档

5.1 insertOne 与 insertMany

javascript
// 插入单条
db.students.insertOne({
    sno: "2021001",
    sname: "张三",
    sage: 20,
    sdept: "计算机系"
})

// 插入多条
db.students.insertMany([
    { sno: "2021002", sname: "李四", sage: 19, sdept: "数学系" },
    { sno: "2021003", sname: "王五", sage: 21, sdept: "计算机系" }
])

插入文档阶段最值得训练的是“对象一次成型”的意识。关系型数据库里,一个完整业务对象常常会分散在多张表逐步写入;在 MongoDB 中,如果一个对象本来就适合整体建模,那么很多核心信息最好在第一次写入时就以完整文档进入系统。这样做的好处是读取路径更短、一致性边界更清晰,也更接近前后端实际交换 JSON 的方式。文档模型的优势,往往正体现在这种“围绕对象整体读写”的能力上。

5.2 插入选项

javascript
//  ordered: false 表示某条失败时继续插入其余文档
db.students.insertMany([...], { ordered: false })

6. 查询文档

6.1 find 基本用法

javascript
// 查询所有文档
db.students.find()

// 格式化输出
db.students.find().pretty()

// 查询单条
db.students.findOne({ sdept: "计算机系" })

// 指定返回字段(1 包含,0 排除,_id 默认包含)
db.students.find({}, { sno: 1, sname: 1, _id: 0 })

MongoDB 查询学习的关键,不是把各种操作符逐条背下来,而是学会顺着文档结构思考。若一个业务对象被设计为层级清晰的文档,那么查询条件、返回字段和更新路径往往也会围绕这个层级结构展开。与关系型数据库相比,MongoDB 查询更强调“对象内部的局部访问”,例如只取某几个字段、只判断某个嵌套路径、只处理数组中的特定元素。理解了这一点,再看查询语法,就会发现它其实是在为文档模型服务,而不是在模仿关系查询。

6.2 条件查询

javascript
// 等值
db.students.find({ sdept: "计算机系" })

// 比较运算
db.students.find({ sage: { $gt: 20 } })   // 大于
db.students.find({ sage: { $gte: 18, $lte: 25 } })  // 区间

// IN
db.students.find({ sdept: { $in: ["计算机系", "数学系"] } })

// 逻辑运算
db.students.find({ $and: [{ sage: { $gte: 18 } }, { sdept: "计算机系" }] })
db.students.find({ $or: [{ sage: { $lt: 18 } }, { sage: { $gt: 25 } }] })
db.students.find({ sdept: { $ne: "物理系" } })

6.3 条件操作符汇总

操作符含义示例
$eq等于{ age: { $eq: 20 } }
$ne不等于{ dept: { $ne: "X" } }
$gt, $gte大于,大于等于{ age: { $gt: 18 } }
$lt, $lte小于,小于等于{ age: { $lte: 60 } }
$in成员{ dept: { $in: ["A","B"] } }
$nin非成员{ dept: { $nin: ["X"] } }
$exists字段存在{ phone: { $exists: true } }
$regex正则{ name: { $regex: "^张" } }
$type类型{ age: { $type: "int" } }

6.4 数组查询

javascript
// 数组包含某元素
db.students.find({ courses: "C001" })

// 数组精确匹配
db.students.find({ courses: ["C001", "C002"] })

// $all:包含所有指定元素
db.students.find({ courses: { $all: ["C001", "C002"] } })

// $elemMatch:数组元素满足多条件
db.scores.find({ scores: { $elemMatch: { subject: "数学", score: { $gte: 90 } } } })

// $size:数组长度
db.students.find({ courses: { $size: 3 } })

数组查询是 MongoDB 很有代表性的能力,因为现实对象里经常天然包含“一个对象拥有多个子值”的结构。课程列表、标签列表、收货地址、评论片段都属于这种情况。若数组中的元素规模可控、生命周期和父对象一致,数组建模会非常自然;但若数组可能无限增长,例如长期累积的日志、评论流、行为轨迹,就必须提前警惕文档膨胀问题。也就是说,数组能力越方便,越需要在建模时判断增长边界。

6.5 嵌套文档查询

javascript
// 精确匹配整个嵌套对象
db.students.find({ "address.city": "北京" })

// 点号访问嵌套字段
db.students.find({ "address.city": "北京", "address.street": /^中关村/ })

嵌套文档查询最能体现 MongoDB 的文档思维。对关系数据库来说,地址往往要拆成多列甚至单独表;对 MongoDB 来说,只要地址本来就是某个对象的内部组成部分,就可以保留层级结构直接查询。这种表达方式非常自然,但也要求设计者清楚哪些结构是真正稳定的从属关系,哪些只是暂时看起来可以嵌套。嵌套结构一旦选错方向,后续独立更新和跨对象复用都会变得麻烦。


7. 更新文档

7.1 updateOne 与 updateMany

javascript
// 更新单条
db.students.updateOne(
    { sno: "2021001" },
    { $set: { sage: 21, sdept: "软件工程" } }
)

// 更新多条
db.students.updateMany(
    { sdept: "计算机系" },
    { $set: { sdept: "计算机科学与技术" } }
)

7.2 更新操作符

操作符含义示例
$set设置字段值{ $set: { age: 21 } }
$unset删除字段{ $unset: { phone: "" } }
$inc数值加减{ $inc: { score: 5 } }
$push数组追加{ $push: { courses: "C003" } }
$pull数组移除{ $pull: { courses: "C001" } }
$addToSet数组去重添加{ $addToSet: { tags: "VIP" } }

7.3 upsert

若匹配不到文档,则插入新文档:

javascript
db.students.updateOne(
    { sno: "2021999" },
    { $set: { sno: "2021999", sname: "新同学", sage: 18 } },
    { upsert: true }
)

MongoDB 虽然已经支持多文档事务,但它更鼓励开发者先通过合理建模,把一致性问题尽量收敛到单文档内部。因为单文档原子性是 MongoDB 非常自然的能力,只要一个业务动作能完整落在一个文档上,数据库就能较低成本地保证写入一致。也就是说,MongoDB 的设计思路通常是先通过建模减少跨文档事务需求,再在确有必要时使用事务,而不是一开始就默认所有业务都依赖复杂事务协调。


8. 删除文档

javascript
// 删除单条
db.students.deleteOne({ sno: "2021999" })

// 删除多条
db.students.deleteMany({ sdept: "已撤销系" })

// 删除集合内所有文档(保留集合结构)
db.students.deleteMany({})

9. 投影、排序与分页

9.1 投影

javascript
db.students.find(
    { sdept: "计算机系" },
    { sno: 1, sname: 1, sage: 1, _id: 0 }
)

9.2 排序 (sort)

javascript
// 升序 1,降序 -1
db.students.find().sort({ sage: -1, sno: 1 })

9.3 分页 (limit, skip)

javascript
// 取前 10 条
db.students.find().limit(10)

// 跳过 20 条,取 10 条(第二页,每页 10 条)
db.students.find().sort({ sno: 1 }).skip(20).limit(10)

NOTE

大数据量下 skip 性能较差,可考虑基于上一页最后一条的游标分页。

分页问题在 MongoDB 中尤其需要结合访问规模理解。很多入门示例都会使用 skip + limit,因为它直观,但当集合很大时,数据库仍需跳过前面大量记录,代价会快速上升。也正因为如此,生产系统里更推荐基于稳定排序键和上一页游标继续向后翻页。这个例子很典型地说明,MongoDB 的很多命令在小数据量下都容易使用,但一旦数据和访问规模增长,就必须把实现方式和底层代价一起考虑。


10. 聚合框架 (Aggregation)

聚合管道由多个阶段组成,每个阶段对文档进行转换,结果传入下一阶段。

10.1 常用阶段

阶段说明
$match过滤文档
$group分组聚合
$project投影与计算字段
$sort排序
$limit / $skip限制与跳过
$lookup多集合关联(类似 JOIN)
$unwind展开数组
$count计数

聚合框架的真正价值,不只是“MongoDB 也能做分组统计”,而是它训练了一种数据流式思维。每个阶段都把上一阶段输出的文档继续加工:$match 负责缩小范围,$project 负责重组字段,$group 负责形成汇总结果,$unwind 负责把数组展开成可继续处理的行式结构。只要把每个阶段都看成一次明确的数据变换,而不是一长串难记的命令,复杂聚合就会容易理解得多。同时也要注意,聚合管道越长、跨集合处理越多,中间结果和内存压力就越大,因此聚合设计仍然需要与建模方式和索引路径一起考虑。

10.2 聚合示例

javascript
// 按系统计人数和平均年龄
db.students.aggregate([
    { $match: { sage: { $gte: 18 } } },
    { $group: { _id: "$sdept", count: { $sum: 1 }, avgAge: { $avg: "$sage" } } },
    { $sort: { count: -1 } },
    { $limit: 5 }
])
javascript
// 多集合关联
db.orders.aggregate([
    { $lookup: {
        from: "products",
        localField: "productId",
        foreignField: "_id",
        as: "productInfo"
    }},
    { $unwind: "$productInfo" }
])

11. 索引

11.1 创建索引

javascript
// 单字段索引
db.students.createIndex({ sno: 1 })

// 唯一索引
db.students.createIndex({ sno: 1 }, { unique: true })

// 复合索引
db.students.createIndex({ sdept: 1, sage: -1 })

// 查看索引
db.students.getIndexes()

11.2 索引类型

  • 单字段 / 复合索引:加速查询和排序。
  • 多键索引:对数组字段自动创建,索引数组中的每个元素。
  • 文本索引:全文检索。
  • 地理空间索引:2dsphere 用于球面几何。

11.3 索引管理

javascript
// 删除索引
db.students.dropIndex("sno_1")

// 查看执行计划
db.students.find({ sno: "2021001" }).explain("executionStats")

MongoDB 的索引思想与关系数据库并不冲突,本质上同样是在用额外结构换更短的定位路径。真正值得关注的,是查询选择性和访问模式。若一个字段取值高度集中,即使建立索引,也未必能显著减少扫描;若某个数组字段或嵌套字段被频繁作为过滤条件,则相应索引就非常关键。判断索引是否值得建立时,最好连续问三个问题:用户最常按什么条件查,是否还需要按什么顺序排,结果通常要返回多少文档。只有围绕真实访问路径建索引,索引才是资产;脱离访问模式盲目建索引,最终只会增加写入和维护成本。


12. 复制与分片

12.1 副本集 (Replica Set)

MongoDB 通过副本集实现高可用。一个副本集包含一个主节点 (Primary) 和若干从节点 (Secondary)。写操作在主节点执行,数据异步复制到从节点;读操作可配置为从从节点读取以分担负载。

12.2 分片 (Sharding)

分片将数据分布到多个分片服务器,实现水平扩展。需要配置:分片键 (Shard Key)、配置服务器 (Config Server) 和路由服务器 (Mongos)。分片键的选择直接影响数据分布的均匀性和查询性能。

分片设计里最需要讲透的就是分片键。分片键并不是部署时随手填写的一个参数,而是系统长期的数据分布规则。若选择了持续递增且天然带热点的字段,新的写入可能长期集中在少数分片上;若选择了区分度很低的字段,数据分布可能严重不均;若分片键与高频查询路径不匹配,又会增加跨分片路由与聚合成本。因此,选择分片键时必须同时考虑未来写入模式、查询模式和增长趋势。MongoDB 的横向扩展能力是否真正发挥出来,很多时候就取决于这一决策是否合理。


13. 备份、恢复与监控

13.1 备份

bash
# mongodump 逻辑备份
mongodump --db school --out /backup/

# 恢复
mongorestore --db school /backup/school/

13.2 监控

  • db.serverStatus():服务器状态。
  • db.stats():当前数据库统计。
  • db.collection.stats():集合统计。
  • MongoDB Atlas / 自建监控可集成 Prometheus、Grafana 等。

对 MongoDB 来说,监控不仅是性能问题,更是结构治理问题。因为模式灵活、集合可能快速增长、分片和副本集又会引入额外复杂度,所以需要持续关注集合大小、索引命中率、慢查询、复制延迟、热点键分布和文档膨胀情况。很多 MongoDB 系统在前期开发速度很快,真正暴露问题往往是在数据规模和访问规模同时上升之后。越早建立这些监控视角,越能避免文档模型从“灵活”演变成“失控”。


14. 与关系型数据库的选型对比

场景更适用 MongoDB更适用关系型
模式频繁变化
强事务、多表关联
水平扩展、高吞吐
复杂分析、报表
半结构化、嵌套数据
金融、账务等强一致性

MongoDB 适合文档型、读写并发高、模式灵活的场景;关系型数据库适合强一致性、复杂关联和事务场景。实践中可根据业务特点混合使用。

从教学角度看,选型问题不应被理解成“MongoDB 和关系型谁更先进”,而应理解成“哪一种数据模型更贴近当前问题”。若业务对象天然是层级结构,字段变化频繁,读路径主要围绕单对象展开,MongoDB 往往更自然;若业务核心高度依赖强事务、多表关联和严格约束,关系型数据库通常更稳妥。MongoDB 真正的重要性,不只是提供了一个具体产品,更是提醒我们:数据库模型本身就是系统设计的一部分,不能脱离业务边界和治理成本去谈工具选择。

与此同时,文档数据库的“灵活”绝不等于“可以不治理”。越是不强制固定结构,越要靠团队自觉建立字段命名规范、Schema 校验、版本演进规则、索引评审和慢查询监控机制。否则,同一集合在不同阶段写入出结构漂移,维护成本会迅速升高。真正成熟的 MongoDB 使用方式,一定同时具备对象建模意识、查询路径意识和长期治理意识。

🔒 会员专属内容

检查登录状态中...