Skip to content

MongoDB 从零到生产级掌握

MongoDB 不能只理解成“能存 JSON 的数据库”。商业项目里真正要掌握的是:为什么文档模型能减少 join,什么时候嵌入什么时候引用,索引为什么能让查询变快,聚合管道怎么一步步处理文档,副本集怎么保证高可用,分片怎么扩展容量,事务为什么不能滥用,线上慢查询和数据倾斜怎么排查。

一句话建立主线:

MongoDB 是文档型数据库,用 BSON 文档组织数据,通过文档建模、索引、聚合、副本集、分片和事务能力支撑灵活结构和高吞吐读写;但它仍然需要围绕查询模式设计文档和索引。

如果你想检查自己是否真的从零基础学懂到能面试、能落地、能排查,按 MongoDB 从零到精通验收清单 逐项验收。

学习目标

学完这一页,你要能做到:

  1. 解释 database、collection、document、field、index 的关系。
  2. 解释 MongoDB 和 MySQL 的建模差异。
  3. 判断数据应该嵌入文档还是拆集合引用。
  4. 写基础 CRUD、条件查询、更新数组、分页和排序。
  5. 解释单字段索引、复合索引、多键索引、唯一索引、TTL 索引。
  6. 看懂 explain("executionStats") 中的关键指标。
  7. 解释聚合管道 $match$project$group$sort$lookup 的执行思路。
  8. 解释副本集 Primary、Secondary、oplog、选举、读写关注。
  9. 解释分片集群 shard key、mongos、config server、chunk 和数据倾斜。
  10. 解释 MongoDB 事务适用边界。
  11. 能排查慢查询、索引失效、文档过大、分片热点、复制延迟。

为什么需要文档数据库

关系库擅长结构稳定、关系明确、强事务的业务,例如订单、支付、库存、账务。MongoDB 更适合文档天然聚合、字段变化快、嵌套结构明显的业务。

场景关系库做法MongoDB 做法
商品详情商品表、图片表、属性表、规格表多表 join一个商品文档中嵌入详情和有限属性
用户画像多张扩展表或 EAV 模型一个用户画像文档保存灵活字段
埋点事件固定宽表或日志表事件文档保存不同事件字段
数据资产扩展属性关系表频繁加字段扩展属性放嵌套对象

MongoDB 的优势不是“不要设计”,而是“按访问模式设计文档”。不设计的 MongoDB 会比关系库更难维护。

MongoDB 核心结构

mermaid
flowchart TD
    A["Database"] --> B["Collection"]
    B --> C["Document"]
    C --> D["Field"]
    B --> E["Index"]

示例文档:

javascript
{
  _id: ObjectId("..."),
  assetCode: "DATASET_001",
  name: "门诊就诊明细",
  ownerDept: "信息科",
  tags: ["医疗", "门诊", "数据资产"],
  fields: [
    { name: "patient_id", type: "string", sensitive: true },
    { name: "visit_time", type: "date", sensitive: false }
  ],
  createdAt: ISODate("2026-07-05T10:00:00Z")
}

这个文档适合 MongoDB 的原因:

  1. 数据资产详情通常整体读取。
  2. 字段列表数量有限。
  3. 扩展字段可能变化。
  4. 标签、字段、敏感标识天然是嵌套结构。

嵌入还是引用

mermaid
flowchart TD
    A["要建模的数据"] --> B{"是否经常一起读取"}
    B -- "是" --> C{"数量是否有限"}
    C -- "是" --> D["优先嵌入"]
    C -- "否" --> E["拆集合引用"]
    B -- "否" --> E
    E --> F["通过业务 ID 关联查询"]
关系推荐原因
用户和少量地址嵌入一起读取,数量有限
商品和规格参数嵌入商品详情一起展示
用户和订单引用订单无限增长,生命周期不同
文章和评论看规模少量评论可嵌入,海量评论要拆
数据资产和字段定义通常嵌入字段定义随资产详情一起展示
数据资产和访问日志引用日志无限增长

如果把无限增长数组放进一个文档,会导致:

  1. 文档越来越大,接近 16MB 限制。
  2. 更新数组越来越慢。
  3. 单文档热点严重。
  4. 很难按数组元素做复杂分页。

CRUD Demo

插入:

javascript
db.data_asset.insertOne({
  assetCode: "DATASET_001",
  name: "门诊就诊明细",
  ownerDept: "信息科",
  status: "PUBLISHED",
  tags: ["医疗", "门诊"],
  fields: [
    { name: "patient_id", type: "string", sensitive: true },
    { name: "visit_time", type: "date", sensitive: false }
  ],
  createdAt: new Date()
})

查询:

javascript
db.data_asset.find({
  ownerDept: "信息科",
  status: "PUBLISHED",
  tags: "医疗"
}, {
  assetCode: 1,
  name: 1,
  ownerDept: 1,
  createdAt: 1
}).sort({ createdAt: -1 }).limit(20)

更新嵌套数组:

javascript
db.data_asset.updateOne(
  { assetCode: "DATASET_001", "fields.name": "patient_id" },
  { $set: { "fields.$.sensitive": true } }
)

分页建议:

javascript
db.data_asset.find({
  status: "PUBLISHED",
  createdAt: { $lt: ISODate("2026-07-05T10:00:00Z") }
}).sort({ createdAt: -1 }).limit(20)

大集合不建议深分页 skip 很大,因为前面的数据仍然要扫描跳过。更适合基于时间或 _id 的游标分页。

一次查询怎么执行

mermaid
flowchart TD
    A["Driver 发送查询"] --> B["mongod 解析 filter/sort/projection"]
    B --> C["查询优化器选择计划"]
    C --> D{"是否有合适索引"}
    D -- "是" --> E["IXSCAN 扫索引"]
    E --> F["FETCH 读取文档"]
    D -- "否" --> G["COLLSCAN 扫集合"]
    F --> H["过滤、排序、投影"]
    G --> H
    H --> I["返回结果"]

explain("executionStats") 重点看:

指标含义判断
winningPlan最终选择的执行计划是否 IXSCAN
totalKeysExamined扫描索引条目数越接近返回数量越好
totalDocsExamined扫描文档数过大说明过滤效率差
nReturned返回文档数和扫描量对比
executionTimeMillis执行耗时是否异常
COLLSCAN集合扫描大集合高风险
SORT内存排序可能需要排序索引

示例:

javascript
db.data_asset.find({
  ownerDept: "信息科",
  status: "PUBLISHED"
}).sort({ createdAt: -1 }).explain("executionStats")

复合索引怎么设计

索引不是字段越多越好,而是匹配查询模式。

常见查询:

javascript
db.data_asset.find({
  ownerDept: "信息科",
  status: "PUBLISHED"
}).sort({ createdAt: -1 })

索引:

javascript
db.data_asset.createIndex({
  ownerDept: 1,
  status: 1,
  createdAt: -1
})

为什么这样排:

  1. ownerDeptstatus 是等值过滤,放前面。
  2. createdAt 用于排序,放后面。
  3. 查询条件和排序方向能利用同一个复合索引。

常见索引类型:

索引作用场景
单字段索引按一个字段查询assetCode
复合索引多条件过滤和排序ownerDept + status + createdAt
多键索引数组字段索引tags
唯一索引防重复assetCode
TTL 索引自动过期临时 token、短期日志

多键索引要注意数组字段过多会让索引膨胀,写入成本变高。

聚合管道全过程

聚合管道像流水线,一段一段处理文档。

mermaid
flowchart TD
    A["原始文档"] --> B["$match 过滤"]
    B --> C["$project 裁剪字段"]
    C --> D["$group 分组统计"]
    D --> E["$sort 排序"]
    E --> F["$limit 限制数量"]

统计每个科室已发布资产数量:

javascript
db.data_asset.aggregate([
  { $match: { status: "PUBLISHED" } },
  { $group: { _id: "$ownerDept", total: { $sum: 1 } } },
  { $sort: { total: -1 } },
  { $limit: 20 }
])

优化原则:

  1. $match 尽量放前面,减少后续文档数量。
  2. $project 尽早裁剪无用字段。
  3. $sort 尽量利用索引。
  4. $lookup 要控制规模,关联字段建索引。
  5. 大报表考虑预聚合,不要每次扫全量。

副本集原理

副本集解决高可用和数据冗余。

mermaid
flowchart TD
    A["Client 写入"] --> B["Primary"]
    B --> C["写 oplog"]
    C --> D["Secondary 1 拉取 oplog"]
    C --> E["Secondary 2 拉取 oplog"]
    D --> F["重放操作"]
    E --> G["重放操作"]

Primary 故障时会选举:

mermaid
flowchart TD
    A["Primary 故障"] --> B["Secondary 发现心跳异常"]
    B --> C["发起选举"]
    C --> D["多数节点投票"]
    D --> E["新 Primary 产生"]
    E --> F["客户端重新路由写入"]

关键概念:

概念含义
Primary接收写入的主节点
Secondary复制 oplog 的从节点
oplog复制操作日志
writeConcern写入确认级别
readConcern读取一致性级别
readPreference读主还是读从

如果读从节点,要接受复制延迟带来的短暂旧数据风险。

分片集群原理

分片解决单机容量和吞吐瓶颈。

mermaid
flowchart TD
    A["Client"] --> B["mongos 路由"]
    B --> C["Config Server 路由元数据"]
    B --> D["Shard 1"]
    B --> E["Shard 2"]
    B --> F["Shard 3"]

分片键非常关键。选错会出现:

问题表现
低基数很多数据落到少数分片
单调递增新写入集中到一个分片
查询不带分片键广播到所有分片
热点 key单个分片 CPU/IO 很高

示例:

javascript
sh.shardCollection("asset_db.data_asset", { ownerDept: 1, assetCode: 1 })

真实生产要根据数据量、查询条件、写入分布评估,不能随便拿自增 ID 当分片键。

MongoDB 事务边界

MongoDB 支持多文档事务,但它不是鼓励你把关系库模型照搬过来。

适合事务:

  1. 少量文档。
  2. 短事务。
  3. 明确业务边界。
  4. 偶尔需要跨集合一致。

不适合事务:

  1. 大批量长事务。
  2. 跨大量分片事务。
  3. 高频核心交易强一致链路。
  4. 本来可以通过文档嵌入解决的更新。

事务 Demo:

javascript
const session = db.getMongo().startSession()
session.startTransaction()

try {
  const asset = session.getDatabase("asset_db").data_asset
  const audit = session.getDatabase("asset_db").asset_audit

  asset.updateOne(
    { assetCode: "DATASET_001" },
    { $set: { status: "PUBLISHED" } }
  )

  audit.insertOne({
    assetCode: "DATASET_001",
    action: "PUBLISH",
    createdAt: new Date()
  })

  session.commitTransaction()
} catch (e) {
  session.abortTransaction()
  throw e
}

商业场景

数据资产扩展属性

医疗数据资产平台中,不同数据集字段结构差异很大,扩展属性经常变化。可以用 MongoDB 保存资产详情扩展:

  1. MySQL 保存核心主数据:资产 ID、编码、状态、权限。
  2. MongoDB 保存扩展详情:字段列表、标签、血缘、展示配置。
  3. Elasticsearch 保存搜索索引。

这样做的原因:

  1. 核心交易状态仍用关系库保证一致性。
  2. 灵活结构交给 MongoDB。
  3. 全文检索交给 ES。
  4. 不把一个数据库强行用于所有场景。

埋点和日志事件

事件字段经常变化,MongoDB 可以保存原始事件文档。但查询必须设计好索引,例如:

javascript
db.event_log.createIndex({ eventType: 1, createdAt: -1 })
db.event_log.createIndex({ userId: 1, createdAt: -1 })

如果只写不管,集合会快速膨胀。要考虑归档、TTL、冷热分层和聚合统计。

线上排查流程

mermaid
flowchart TD
    A["MongoDB 线上问题"] --> B{"表现"}
    B -- "查询慢" --> C["explain 看 IXSCAN/COLLSCAN"]
    B -- "写入慢" --> D["看索引数量、锁、磁盘、writeConcern"]
    B -- "复制延迟" --> E["看 oplog、Secondary 延迟"]
    B -- "分片倾斜" --> F["看 shard key 和 chunk 分布"]
    B -- "内存高" --> G["看工作集、索引大小、聚合"]
    C --> H["totalDocsExamined 是否远大于 nReturned"]

常用命令:

javascript
db.data_asset.find({ status: "PUBLISHED" }).explain("executionStats")
db.data_asset.getIndexes()
db.currentOp()
rs.status()
sh.status()
db.serverStatus()

排查表:

现象可能原因处理
查询慢无索引、索引顺序不对、内存排序建复合索引,用 explain 验证
写入慢索引太多、磁盘慢、writeConcern 高精简索引,检查磁盘
文档更新慢文档过大、数组无限增长拆集合,控制文档大小
从库读旧数据复制延迟读主或调整一致性要求
分片热点shard key 单调或低基数重新评估分片键
聚合慢$match 太晚、$lookup调整管道和预聚合

面试标准回答

MongoDB 适合什么场景

text
MongoDB 适合文档天然聚合、字段变化较快、嵌套结构明显、读写吞吐较高的场景,比如商品详情、用户画像、内容草稿、日志事件、数据资产扩展属性。它不适合大量复杂 join、强事务和高度关系化的核心交易场景。MongoDB 不是随便存 JSON,仍要按查询模式设计文档和索引。

嵌入和引用怎么选

text
经常一起读取、生命周期接近、数量有限的数据适合嵌入;不经常一起读取、数量无限增长、生命周期不同的数据适合拆集合引用。比如用户地址可以嵌入,用户订单不适合无限追加到用户文档里。

MongoDB 查询为什么快

text
MongoDB 查询快主要依赖合适的索引和合理的文档模型。有索引时可以通过 B-Tree 索引快速定位候选文档,再 FETCH 文档;如果经常一起读取的数据被嵌入在同一文档里,也能减少关系库多表 join 的成本。但没有合适索引时同样会 COLLSCAN,性能会很差。

副本集和分片区别

text
副本集解决高可用和数据冗余,Primary 写入,Secondary 复制 oplog,Primary 故障后选举新主。分片解决容量和吞吐扩展,通过 shard key 把数据分布到多个 shard。副本集关注可用性,分片关注水平扩展。

关联知识点

知识点继续学习
MongoDB 总览开始
从零到精通验收MongoDB 从零到精通验收清单
商业场景训练营MongoDB 商业场景训练营
MongoDB 基础MongoDB基础
核心全过程原理核心全过程原理
索引设计索引设计
聚合管道聚合管道
MongoDB 高级MongoDB高级
MongoDB 面试MongoDB面试
MySQLMySQL 从零到生产级掌握
ElasticsearchES 从零到生产级掌握

本章小结

MongoDB 学到生产级,重点不是会写 insertOnefind,而是能根据查询模式设计文档,能用索引支撑过滤和排序,能用聚合管道处理统计,能理解副本集和分片的边界,能知道事务为什么不能滥用,并且能用 explain、oplog、分片状态和监控指标定位线上问题。