Skip to content

MongoDB 商业场景训练营

这一页不是重复命令手册,而是把 MongoDB 放到商业系统里讲清楚:什么时候该用文档模型,什么时候不要用;文档怎么设计才不会后期爆炸;索引为什么能快;副本集、分片、事务、慢查询和数据倾斜出问题时怎么排查。

一句话主线:

MongoDB 的核心能力是“按业务对象组织文档”,但生产可用靠的是建模边界、索引命中、读写关注、副本集高可用、分片均衡和可观测排查。

训练目标

学完本页,你应该能做到:

  1. 为数据资产、商品详情、用户画像、埋点事件设计合理文档。
  2. 判断嵌入、引用、冗余字段、反范式设计的边界。
  3. 写出可执行的 CRUD、索引、聚合、事务 Demo。
  4. 看懂 explain("executionStats"),知道慢在哪里。
  5. 解释副本集和分片集群的工作原理。
  6. 排查慢查询、索引失效、复制延迟、分片热点、文档过大。
  7. 在面试里讲出 MongoDB 为什么快、为什么不能乱用事务、为什么不能无限数组。

商业场景一:医疗数据资产详情

数据资产平台常见需求是展示一个数据集的基础信息、字段列表、敏感等级、标签、归属部门、血缘摘要。这个对象天然适合文档模型,因为页面经常一次性读取完整详情。

javascript
db.data_asset.insertOne({
  assetCode: "DATASET_OUTPATIENT_VISIT",
  name: "门诊就诊明细",
  ownerDept: "信息科",
  domain: "医疗数据",
  status: "PUBLISHED",
  tags: ["门诊", "患者", "数据资产"],
  securityLevel: "SENSITIVE",
  fields: [
    { name: "patient_id", type: "string", sensitive: true, comment: "患者唯一标识" },
    { name: "visit_time", type: "date", sensitive: false, comment: "就诊时间" },
    { name: "dept_code", type: "string", sensitive: false, comment: "科室编码" }
  ],
  lineage: {
    sourceSystem: "HIS",
    syncMode: "DAILY_BATCH"
  },
  createdAt: new Date(),
  updatedAt: new Date()
})

为什么这样设计:

设计原因不这样会怎样
字段列表嵌入 fields资产详情页通常一起读取,字段数量有限拆成字段集合后每次详情都要二次查询
标签嵌入数组标签短小、查询常见拆表会让简单筛选变复杂
血缘摘要嵌入对象页面展示摘要即可完整血缘图可单独存集合,避免文档膨胀
访问日志不嵌入日志无限增长单文档过大、热点更新、接近 16MB 限制

文档建模决策流程

mermaid
flowchart TD
    A["拿到一个业务对象"] --> B{"是否经常整体读取"}
    B -- "是" --> C{"子数据数量是否有限"}
    C -- "是" --> D["优先嵌入到同一文档"]
    C -- "否" --> E["拆集合引用"]
    B -- "否" --> E
    E --> F{"是否需要高频联查"}
    F -- "是" --> G["保留必要冗余字段"]
    F -- "否" --> H["只保存业务 ID 引用"]
    G --> I["用定时任务或事件保证冗余一致"]
    H --> J["应用层按需二次查询"]

判断口诀:

  1. 一起读、数量有限、生命周期一致,适合嵌入。
  2. 无限增长、独立查询、独立生命周期,适合引用。
  3. MongoDB 允许冗余,但冗余字段必须有同步策略。
  4. 不要为了“文档数据库”把所有东西塞进一个文档。

商业场景二:资产列表查询和索引

列表页常见查询条件:

  1. 按归属部门过滤。
  2. 按状态过滤。
  3. 按标签过滤。
  4. 按创建时间倒序分页。

查询语句:

javascript
db.data_asset.find({
  ownerDept: "信息科",
  status: "PUBLISHED",
  tags: "门诊",
  createdAt: { $lt: ISODate("2026-07-01T00:00:00Z") }
}, {
  assetCode: 1,
  name: 1,
  ownerDept: 1,
  securityLevel: 1,
  createdAt: 1
}).sort({ createdAt: -1 }).limit(20)

推荐索引:

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

为什么复合索引顺序很重要:

字段放在前面的原因
ownerDept等值过滤,能快速缩小部门范围
status等值过滤,继续缩小范围
tags数组匹配,多键索引支持标签筛选
createdAt支持范围分页和排序

如果把 createdAt 放最前面,而查询又主要按部门和状态筛选,索引会先扫一大段时间范围,再过滤部门,扫描量可能明显变大。

查询执行原理

mermaid
flowchart TD
    A["应用发起 find"] --> B["Driver 序列化 BSON"]
    B --> C["mongod 解析查询条件"]
    C --> D["查询优化器生成候选计划"]
    D --> E{"是否能利用索引"}
    E -- "能" --> F["IXSCAN 扫索引键"]
    F --> G["FETCH 读取文档"]
    E -- "不能" --> H["COLLSCAN 全集合扫描"]
    G --> I["过滤、排序、投影"]
    H --> I
    I --> J["批量返回结果给 Driver"]

explain("executionStats") 示例:

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

重点指标:

指标解释怎么判断
stage执行阶段希望看到 IXSCAN,不要长期 COLLSCAN
nReturned返回多少条和扫描量对比
totalKeysExamined扫描多少索引键过大说明索引选择不够精准
totalDocsExamined读取多少文档远大于返回量说明过滤效率差
executionTimeMillis执行耗时结合扫描量判断慢因

商业场景三:资产统计聚合

需求:按部门统计已发布资产数量和敏感资产数量。

javascript
db.data_asset.aggregate([
  { $match: { status: "PUBLISHED" } },
  {
    $group: {
      _id: "$ownerDept",
      total: { $sum: 1 },
      sensitiveCount: {
        $sum: {
          $cond: [{ $eq: ["$securityLevel", "SENSITIVE"] }, 1, 0]
        }
      }
    }
  },
  { $sort: { total: -1 } }
])

聚合管道不是一次性魔法,它是一段一段处理文档:

mermaid
flowchart TD
    A["原始文档集合"] --> B["$match 先过滤状态"]
    B --> C["$group 按部门分组"]
    C --> D["计算 total 和 sensitiveCount"]
    D --> E["$sort 排序"]
    E --> F["返回统计结果"]

优化原则:

  1. $match 尽量放前面,先减少文档数量。
  2. $match 字段尽量有索引。
  3. 大聚合要关注内存和临时文件。
  4. 高频统计不要每次实时全量聚合,可以做预聚合表。

商业场景四:副本集高可用

副本集解决的是单节点故障问题。

mermaid
flowchart TD
    A["应用 Driver"] --> B["Primary"]
    B --> C["写入 oplog"]
    C --> D["Secondary 1 拉取 oplog"]
    C --> E["Secondary 2 拉取 oplog"]
    B --> F{"Primary 故障"}
    F --> G["Secondary 发起选举"]
    G --> H["选出新的 Primary"]
    H --> I["Driver 感知拓扑后继续写入"]

关键概念:

概念说明
Primary接收写请求的主节点
Secondary复制主节点 oplog 的从节点
oplog记录写操作的有序日志
Election主节点故障后选举新主
Write Concern写入要被多少节点确认才算成功
Read Concern读取时对数据可见性的要求

如果写关注只要求主节点确认,性能高但主节点宕机时可能丢失尚未复制的数据。核心业务更常用 majority,让多数节点确认后再返回成功。

商业场景五:分片和热点

分片解决的是单副本集容量和写入吞吐上限。

mermaid
flowchart TD
    A["应用"] --> B["mongos 路由"]
    B --> C["Config Server 保存元数据"]
    B --> D["Shard 1"]
    B --> E["Shard 2"]
    B --> F["Shard 3"]
    D --> G["Chunk 范围 1"]
    E --> H["Chunk 范围 2"]
    F --> I["Chunk 范围 3"]

分片键选择很关键。

分片键优点风险
_id 哈希写入较均匀范围查询不友好
createdAt时间范围查询方便新数据集中写一个分片,容易热点
ownerDept部门查询方便大部门数据倾斜
ownerDept + hash(assetCode)兼顾部门和均匀性查询和索引设计更复杂

分片不是万能扩容按钮。分片键选错后,扩容节点短期可能有效,但热点分片仍然会继续堆积压力。

事务 Demo

MongoDB 支持多文档事务,但不能滥用。适合少量文档、短事务、必须原子一致的场景。

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

try {
  const assets = session.getDatabase("asset_platform").data_asset
  const audit = session.getDatabase("asset_platform").asset_audit

  assets.updateOne(
    { assetCode: "DATASET_OUTPATIENT_VISIT" },
    { $set: { status: "ARCHIVED", updatedAt: new Date() } }
  )

  audit.insertOne({
    assetCode: "DATASET_OUTPATIENT_VISIT",
    action: "ARCHIVE",
    operator: "admin",
    createdAt: new Date()
  })

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

为什么事务不能乱用:

  1. 长事务会占用资源,影响并发。
  2. 分布式事务跨分片成本更高。
  3. 文档模型本来就鼓励把强一致的小对象聚合到单文档里。
  4. 如果大量业务都依赖复杂事务,说明可能更适合关系型数据库。

生产排查流程

mermaid
flowchart TD
    A["发现 MongoDB 查询慢"] --> B["确认慢的是读还是写"]
    B --> C["查看慢日志和 profiler"]
    C --> D["对慢 SQL 执行 explain"]
    D --> E{"是否 COLLSCAN"}
    E -- "是" --> F["补索引或改查询条件"]
    E -- "否" --> G{"扫描量是否远大于返回量"}
    G -- "是" --> H["调整复合索引顺序和过滤字段"]
    G -- "否" --> I{"是否排序或聚合耗时"}
    I -- "是" --> J["让排序命中索引或做预聚合"]
    I -- "否" --> K["检查网络、锁、磁盘、复制延迟"]

常见问题对照:

现象可能原因处理
查询突然慢索引未命中、数据量增长、执行计划变化看慢日志和 explain
写入慢写关注过高、磁盘慢、索引过多检查 write concern、磁盘、索引数量
Secondary 延迟写入量大、节点性能差、网络慢看复制延迟和 oplog
分片不均分片键低基数或热点重新评估分片键和数据迁移
文档过大无限数组或嵌套太深拆集合、只保留摘要

面试标准回答

MongoDB 适合什么场景?

MongoDB 适合文档结构灵活、字段变化快、对象天然聚合、读写吞吐要求较高的场景,比如商品详情、用户画像、内容草稿、埋点事件、数据资产详情。但它不是不用设计结构,仍然要根据查询模式设计文档和索引。强事务、多表复杂关联、账务类系统优先考虑关系型数据库。

MongoDB 为什么查询快?

查询快主要来自合理索引和文档局部性。索引可以减少扫描范围,文档模型可以把经常一起读取的数据放在同一文档里,减少多表 join 和多次网络往返。但如果没有索引、文档过大、聚合不合理,MongoDB 一样会慢。

嵌入和引用怎么选?

一起读取、数量有限、生命周期一致的子数据适合嵌入;无限增长、独立查询、独立生命周期的数据适合引用。比如资产详情里的字段定义可以嵌入,访问日志应该拆集合。

副本集和分片分别解决什么?

副本集解决高可用和数据冗余,Primary 故障后 Secondary 选举新 Primary。分片解决单副本集容量和吞吐上限,通过分片键把数据分散到多个 shard。副本集不等于水平扩容,分片也不能替代副本集高可用。

关联知识点

知识点继续学习
MongoDB 主线MongoDB 从零到生产级掌握
核心原理MongoDB 核心全过程原理
基础 CRUDMongoDB 基础
索引设计MongoDB 索引设计
聚合管道MongoDB 聚合管道
面试题MongoDB 面试