Skip to content

MongoDB 从零到精通验收清单

这页用来验收你是否真的把 MongoDB 学到了能建模、能写查询、能设计索引、能做商业项目、能排查线上问题、能面试回答的程度。

MongoDB 不是“能存 JSON 的 MySQL”,也不是“随便丢文档不用设计”。真正学懂要能解释:

  1. MongoDB 为什么使用 BSON 文档,文档模型和关系模型的差异是什么。
  2. 什么数据适合嵌入,什么数据必须拆集合引用。
  3. 一次查询从 Driver 到 mongod,再到索引扫描和文档返回的全过程。
  4. 为什么有索引仍可能慢,explain("executionStats") 应该看什么。
  5. 复合索引、ESR 原则、多键索引、TTL 索引、唯一索引怎么设计。
  6. 聚合管道为什么要尽早 $match$lookup 为什么要谨慎。
  7. 副本集、oplog、选举、writeConcern、readPreference 怎么影响一致性。
  8. 分片集群中 shard key 为什么决定数据分布、写热点和查询广播。
  9. MongoDB 事务什么时候适合用,什么时候说明建模不合理。
  10. 商业系统中 MongoDB、MySQL、Elasticsearch 应该如何配合。

总学习路线

mermaid
flowchart TD
    A["阶段1:理解文档数据库定位"] --> B["阶段2:BSON、集合、文档和字段"]
    B --> C["阶段3:文档建模:嵌入还是引用"]
    C --> D["阶段4:CRUD、更新数组和分页"]
    D --> E["阶段5:查询执行和 explain"]
    E --> F["阶段6:索引设计和 ESR 原则"]
    F --> G["阶段7:聚合管道和 lookup"]
    G --> H["阶段8:副本集和读写一致性"]
    H --> I["阶段9:分片和 shard key"]
    I --> J["阶段10:事务、商业场景和生产排查"]

这条路线的核心是:MongoDB 的设计入口不是“表结构”,而是“查询模式”。先想清楚业务怎么读、怎么写、数据是否一起变化,再决定文档结构、索引和部署方式。

阶段1:MongoDB 到底解决什么问题

关系型数据库擅长结构稳定、强事务、多表关系清晰的业务。例如订单、支付、库存、账务。

MongoDB 更适合文档天然聚合、字段变化较快、嵌套结构明显、读写吞吐较高的业务。例如:

场景为什么适合 MongoDB
商品详情图片、规格、属性、扩展字段经常整体读取
用户画像字段灵活,标签和偏好变化快
内容草稿嵌套段落、组件配置、版本草稿结构不固定
埋点事件不同事件字段不同,写入量大
医疗数据资产扩展属性不同资产字段、标签、血缘、展示配置差异大

但 MongoDB 不是万能替代 MySQL。

不适合的场景:

场景原因
订单支付账务核心链路强事务和一致性要求高
大量复杂 JoinMongoDB 不是为关系 Join 优化的
高度规范化的主数据关系库约束和事务更合适
跨集合强一致频繁修改建模方式可能不适合 MongoDB

一句话:MongoDB 用文档模型换取灵活结构和更贴近聚合对象的读写方式,但你仍然必须设计数据模型和索引。

阶段2:BSON、集合和文档

MongoDB 文档看起来像 JSON,但底层是 BSON。BSON 是二进制文档格式,支持 ObjectIdDateDecimal128、二进制等类型。

javascript
{
  _id: ObjectId("64f1..."),
  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-06T10:00:00Z")
}

核心概念:

MongoDB关系库类比注意点
databasedatabase数据库
collectiontable集合不强制固定 schema
documentrow文档可以嵌套对象和数组
fieldcolumn字段可以灵活变化
indexindex查询性能仍依赖索引

文档结构:

mermaid
flowchart TD
    A["Database"] --> B["Collection"]
    B --> C["Document"]
    C --> D["普通字段"]
    C --> E["嵌套对象"]
    C --> F["数组字段"]
    B --> G["Index"]

常见误区:

  1. MongoDB 不是存 JSON 字符串,而是 BSON 文档。
  2. 时间、金额、ID 不要全部当字符串存。
  3. 集合 schema 灵活,不代表字段可以没有规范。
  4. _id 默认有唯一索引,但业务唯一性仍要设计业务键。

阶段3:文档建模:嵌入还是引用

MongoDB 建模第一问不是“建几张表”,而是“业务怎么读写”。

mermaid
flowchart TD
    A["一组相关数据"] --> B{"是否经常一起读取"}
    B -- "是" --> C{"数量是否有限"}
    C -- "是" --> D["优先嵌入到同一文档"]
    C -- "否" --> E["拆成独立集合引用"]
    B -- "否" --> E
    E --> F{"是否需要强事务频繁维护"}
    F -- "是" --> G["考虑关系库或重新建模"]
    F -- "否" --> H["用业务 ID 关联查询"]

嵌入适合:

关系原因
用户和少量地址数量有限,经常一起读取
商品和规格参数商品详情页整体展示
资产和字段定义数据资产详情常一起读取
文章和草稿组件文档结构天然聚合

引用适合:

关系原因
用户和订单订单无限增长
文章和海量评论评论数量大,分页独立
资产和访问日志日志无限增长,生命周期不同
设备和采集记录采集记录写入量大

如果把无限增长数组嵌入单文档:

  1. 文档越来越大,接近 16MB 限制。
  2. 更新数组需要移动或重写较大文档。
  3. 单文档成为热点。
  4. 数组分页困难。
  5. 多键索引膨胀,写入成本变高。

阶段4:CRUD 和更新数组

插入数据资产:

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

条件查询和投影:

javascript
db.data_asset.find(
  {
    hospitalId: "H001",
    status: "PUBLISHED",
    deleted: false,
    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.updateOne(
  { assetCode: "DATASET_001" },
  { $addToSet: { tags: "重点资产" } }
)

$push$addToSet 区别:

操作符行为场景
$push直接追加,允许重复追加事件,但不适合无限数组
$addToSet不存在才追加标签、权限码等去重数组

阶段5:分页为什么不能只用 skip

普通分页:

javascript
db.data_asset.find({ status: "PUBLISHED" })
  .sort({ createdAt: -1 })
  .skip(100000)
  .limit(20)

skip 很大时会慢,因为数据库仍要按顺序扫描并丢弃前 100000 条,再返回 20 条。

更适合滚动加载的游标分页:

javascript
db.data_asset.find({
  status: "PUBLISHED",
  $or: [
    { createdAt: { $lt: ISODate("2026-07-06T10:00:00Z") } },
    {
      createdAt: ISODate("2026-07-06T10:00:00Z"),
      _id: { $lt: ObjectId("64f1...") }
    }
  ]
}).sort({ createdAt: -1, _id: -1 }).limit(20)

为什么要带 _id

如果多个文档 createdAt 相同,只用时间做游标可能重复或漏数据。加 _id 作为稳定排序补充字段,可以让分页顺序稳定。

阶段6:一次查询怎么执行

mermaid
flowchart TD
    A["应用发起 find"] --> B["MongoDB Driver 编码 BSON"]
    B --> C["mongod 接收请求"]
    C --> D["解析 filter、sort、projection、limit"]
    D --> E["查询优化器选择执行计划"]
    E --> F{"是否有合适索引"}
    F -- "有" --> G["IXSCAN 扫描索引"]
    G --> H["FETCH 读取文档"]
    F -- "没有" --> I["COLLSCAN 扫描集合"]
    H --> J["过滤、排序、投影"]
    I --> J
    J --> K["返回 BSON 结果"]
    K --> L["Driver 解码成对象"]

explain("executionStats") Demo:

javascript
db.data_asset.find({
  hospitalId: "H001",
  status: "PUBLISHED",
  deleted: false
}).sort({ createdAt: -1 }).explain("executionStats")

重点指标:

指标含义怎么判断
winningPlan选中的执行计划看是否走 IXSCAN
stage: "COLLSCAN"集合扫描大集合高风险
stage: "IXSCAN"索引扫描还要看扫描量
totalKeysExamined扫描索引项数量应接近返回数量
totalDocsExamined扫描文档数量远大于返回量说明过滤差
nReturned返回文档数和扫描量对比
executionTimeMillis执行耗时结合业务 SLA
SORT内存排序可能缺排序索引

有索引但仍慢的常见原因:

  1. 索引选择性差,扫描大量索引。
  2. 复合索引字段顺序不匹配查询。
  3. 排序没有被索引覆盖,出现内存排序。
  4. 查询返回大量文档,FETCH 成本高。
  5. 文档太大,网络和反序列化成本高。

阶段7:索引设计和 ESR 原则

常见查询:

javascript
db.data_asset.find({
  hospitalId: "H001",
  status: "PUBLISHED",
  deleted: false
}).sort({ createdAt: -1 })

可以考虑复合索引:

javascript
db.data_asset.createIndex({
  hospitalId: 1,
  status: 1,
  deleted: 1,
  createdAt: -1,
  _id: -1
})

ESR 原则:

字母含义例子
EEquality 等值条件hospitalIdstatus
SSort 排序字段createdAt
RRange 范围条件$gt$lt

ESR 不是死规则。还要看字段区分度、查询频率、排序方向和 explain 结果。

常见索引类型:

索引作用注意
单字段索引按一个字段查询简单但不能满足多条件排序
复合索引多条件过滤和排序顺序很重要
多键索引数组字段索引数组过大索引膨胀
唯一索引保证业务唯一软删除要考虑唯一冲突
TTL 索引自动过期删除不适合核心业务数据

索引不是越多越好:

  1. 每次写入都要维护索引。
  2. 索引占磁盘和内存。
  3. 多个相似索引增加优化器选择成本。
  4. 无用索引拖慢写入。

阶段8:覆盖查询

覆盖查询是指查询所需字段都在索引里,不需要 FETCH 原始文档。

javascript
db.data_asset.createIndex({
  hospitalId: 1,
  status: 1,
  createdAt: -1,
  assetCode: 1,
  name: 1
})

查询:

javascript
db.data_asset.find(
  { hospitalId: "H001", status: "PUBLISHED" },
  { _id: 0, assetCode: 1, name: 1, createdAt: 1 }
).sort({ createdAt: -1 })

覆盖查询适合列表页只展示少量字段的场景。但不要为了覆盖把大量字段塞进索引,否则索引会变大,写入变慢。

阶段9:聚合管道全过程

聚合管道是一条文档处理流水线。

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

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

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

为什么 $match 要尽量靠前?

因为后面的 $group$sort$lookup 都是重操作。越早过滤,后续处理文档越少,CPU、内存和磁盘压力越小。

$lookup 风险:

javascript
db.data_asset.aggregate([
  { $match: { hospitalId: "H001" } },
  {
    $lookup: {
      from: "department",
      localField: "ownerDeptId",
      foreignField: "_id",
      as: "dept"
    }
  }
])

$lookup 要注意:

  1. 主集合先 $match 缩小范围。
  2. foreignField 要建索引。
  3. 控制返回字段。
  4. 高频关联字段可适当冗余。
  5. 复杂报表考虑预聚合或数仓。

阶段10:副本集原理

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

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["选出新 Primary"]
    D --> E["客户端重新发现拓扑"]
    E --> F["写入切换到新 Primary"]

核心概念:

概念说明
Primary接收写入
Secondary复制 oplog 并重放
oplog复制操作日志
writeConcern写入确认级别
readConcern读取可见性
readPreference读主还是读从

writeConcern

设置含义取舍
w: 1Primary 写成功就返回延迟低,可靠性弱
w: "majority"多数节点确认才返回更可靠,延迟更高
j: true写 journal 后确认更可靠,成本更高

读从库的风险:Secondary 可能有复制延迟,用户刚写完马上读可能读到旧数据。

阶段11:副本集不能替代备份

副本集解决的是节点故障,不解决误删误改。

mermaid
flowchart TD
    A["Primary 误删数据"] --> B["写入 oplog"]
    B --> C["Secondary 同步误删操作"]
    C --> D["所有副本都删除了数据"]

所以生产必须有:

  1. 定期备份。
  2. 备份可恢复验证。
  3. 关键集合误删恢复预案。
  4. 审计和权限控制。
  5. 高风险操作审批。

“有三个副本”不等于“有备份”。

阶段12:分片集群原理

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

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

查询带 shard key:

mermaid
flowchart TD
    A["查询带 hospitalId"] --> B["mongos 根据路由表定位分片"]
    B --> C["只访问目标 Shard"]
    C --> D["返回结果"]

查询不带 shard key:

mermaid
flowchart TD
    A["查询不带 shard key"] --> B["mongos 无法定位单个分片"]
    B --> C["广播到所有 Shard"]
    C --> D["汇总结果"]
    D --> E["延迟和资源消耗上升"]

shard key 选择错误的后果:

错误 shard key后果
低基数字段,如 status数据严重倾斜
单调递增字段,如 createdAt新写入集中到一个分片
查询很少携带的字段大量广播查询
热点业务 ID单个分片压力高

分片不是性能问题第一解。先优化索引、查询、文档模型、归档、读写分离,再评估分片。

阶段13:事务边界

MongoDB 支持事务,但事务不是默认推荐的建模方式。

适合事务:

场景原因
少量文档跨集合一致修改边界清晰
状态变更和审计记录一起写短事务
管理后台低频强一致操作量小、可控

不适合事务:

场景原因
大批量导入事务太长,资源占用大
跨大量分片修改延迟和协调成本高
高频交易核心链路关系库更合适
本可嵌入的数据还跨集合事务建模可能不合理

事务 Demo:

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

try {
  const assetDb = session.getDatabase("asset_db")

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

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

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

事务越多,越要反思文档模型是否把本该一起变化的数据拆散了。

阶段14:商业项目架构组合

医疗数据资产平台可以这样组合:

mermaid
flowchart TD
    A["MySQL"] --> B["核心主数据<br/>资产编号、状态、权限、流程"]
    C["MongoDB"] --> D["扩展属性<br/>字段列表、标签、配置、血缘快照"]
    E["Elasticsearch"] --> F["全文搜索<br/>资产名称、字段、描述"]
    G["Redis"] --> H["热点缓存<br/>权限、字典、临时状态"]

为什么不只用一个数据库?

数据更适合原因
资产状态、审批流程MySQL强一致、事务、关系清晰
字段定义、展示配置MongoDB结构灵活、文档聚合
搜索资产和字段Elasticsearch倒排索引、相关性排序
权限缓存和临时数据Redis低延迟、高并发

商业选型原则:不同数据库解决不同问题,不要把 MongoDB 当搜索引擎,也不要把 MySQL 当文档扩展字段垃圾桶。

阶段15:生产排查

查询慢

mermaid
flowchart TD
    A["查询慢"] --> B["打印 filter、sort、projection"]
    B --> C["执行 explain executionStats"]
    C --> D["是否 COLLSCAN"]
    D --> E["totalDocsExamined 是否远大于 nReturned"]
    E --> F["是否出现 SORT"]
    F --> G["索引顺序是否匹配查询"]
    G --> H["文档是否过大或返回字段太多"]

写入慢

mermaid
flowchart TD
    A["写入慢"] --> B["索引数量是否过多"]
    B --> C["文档是否过大"]
    C --> D["数组是否频繁增长"]
    D --> E["writeConcern 是否过高"]
    E --> F["磁盘 IO 是否瓶颈"]
    F --> G["是否单文档或单分片热点"]

复制延迟

mermaid
flowchart TD
    A["复制延迟"] --> B["Secondary 是否资源不足"]
    B --> C["Primary 写入是否突增"]
    C --> D["oplog 窗口是否足够"]
    D --> E["网络是否异常"]
    E --> F["是否有大事务或大批量写入"]

分片倾斜

mermaid
flowchart TD
    A["分片倾斜"] --> B["检查 shard key 基数"]
    B --> C["检查 chunk 分布"]
    C --> D["检查写入是否集中到单分片"]
    D --> E["查询是否大量广播"]
    E --> F["评估重新分片或调整模型"]

常用命令:

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

阶段16:常见坑和后果

后果正确做法
把 MongoDB 当随便存 JSON字段混乱,查询不可维护建字段规范和文档模型
无限数组嵌入文档文档过大,更新慢拆集合引用
看到字段就建索引写入变慢,索引膨胀按查询模式建索引
只看到 IXSCAN 就以为快扫描量仍可能很大看 keys/docs examined
大量使用 skip 深分页越翻越慢游标分页
高频 $lookup退化成关系 Join冗余或重新建模
无脑读从库读到旧数据按一致性要求选择
用副本集替代备份误删同步到所有副本必须做备份和恢复演练
随便选 shard key数据倾斜、写热点根据查询和写入分布设计
滥用事务延迟和资源占用上升优先合理文档建模

阶段17:面试标准回答

问:MongoDB 适合什么场景?

标准回答:

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

问:嵌入和引用怎么选?

标准回答:

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

问:MongoDB 查询为什么快?

标准回答:

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

问:副本集和分片区别?

标准回答:

副本集解决高可用和数据冗余,Primary 接收写入,Secondary 复制 oplog,Primary 故障后通过选举产生新主。分片解决容量和吞吐扩展,通过 shard key 把数据分布到多个 shard,mongos 根据路由表转发请求。副本集关注可用性,分片关注水平扩展。

最终验收题

如果下面问题答不清楚,说明还没有真正掌握 MongoDB:

  1. MongoDB 文档模型和 MySQL 表模型最大差异是什么?
  2. BSON 和 JSON 有什么区别?
  3. 为什么业务唯一键不能完全依赖 ObjectId?
  4. 嵌入和引用怎么选?无限数组有什么风险?
  5. skip 深分页为什么慢?游标分页为什么要加 _id
  6. explain("executionStats")totalDocsExamined 远大于 nReturned 说明什么?
  7. 有 IXSCAN 为什么仍然可能慢?
  8. ESR 原则怎么用于复合索引设计?
  9. 多键索引为什么可能膨胀?
  10. 聚合管道为什么要先 $match
  11. $lookup 为什么不能滥用?
  12. oplog 是什么?Secondary 为什么会延迟?
  13. writeConcern 和 readPreference 分别影响什么?
  14. 为什么副本集不能替代备份?
  15. shard key 选错会有哪些后果?
  16. MongoDB 事务为什么不能滥用?
  17. 医疗数据资产平台中 MySQL、MongoDB、ES 应该怎么分工?

关联知识点跳转

本章小结

MongoDB 学到精通,关键是理解文档模型背后的取舍:用嵌入减少关联读取,用索引支撑查询和排序,用聚合管道处理统计,用副本集保证高可用,用分片扩展容量。但它仍然需要设计文档结构、索引、备份、权限和排查流程。会 CRUD 只是入门,能解释查询为什么慢、索引为什么不生效、分片为什么倾斜、事务为什么不该滥用,才算真正掌握。