MongoDB 商业场景训练营
这一页不是重复命令手册,而是把 MongoDB 放到商业系统里讲清楚:什么时候该用文档模型,什么时候不要用;文档怎么设计才不会后期爆炸;索引为什么能快;副本集、分片、事务、慢查询和数据倾斜出问题时怎么排查。
一句话主线:
MongoDB 的核心能力是“按业务对象组织文档”,但生产可用靠的是建模边界、索引命中、读写关注、副本集高可用、分片均衡和可观测排查。
训练目标
学完本页,你应该能做到:
- 为数据资产、商品详情、用户画像、埋点事件设计合理文档。
- 判断嵌入、引用、冗余字段、反范式设计的边界。
- 写出可执行的 CRUD、索引、聚合、事务 Demo。
- 看懂
explain("executionStats"),知道慢在哪里。 - 解释副本集和分片集群的工作原理。
- 排查慢查询、索引失效、复制延迟、分片热点、文档过大。
- 在面试里讲出 MongoDB 为什么快、为什么不能乱用事务、为什么不能无限数组。
商业场景一:医疗数据资产详情
数据资产平台常见需求是展示一个数据集的基础信息、字段列表、敏感等级、标签、归属部门、血缘摘要。这个对象天然适合文档模型,因为页面经常一次性读取完整详情。
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 限制 |
文档建模决策流程
flowchart TD
A["拿到一个业务对象"] --> B{"是否经常整体读取"}
B -- "是" --> C{"子数据数量是否有限"}
C -- "是" --> D["优先嵌入到同一文档"]
C -- "否" --> E["拆集合引用"]
B -- "否" --> E
E --> F{"是否需要高频联查"}
F -- "是" --> G["保留必要冗余字段"]
F -- "否" --> H["只保存业务 ID 引用"]
G --> I["用定时任务或事件保证冗余一致"]
H --> J["应用层按需二次查询"]判断口诀:
- 一起读、数量有限、生命周期一致,适合嵌入。
- 无限增长、独立查询、独立生命周期,适合引用。
- MongoDB 允许冗余,但冗余字段必须有同步策略。
- 不要为了“文档数据库”把所有东西塞进一个文档。
商业场景二:资产列表查询和索引
列表页常见查询条件:
- 按归属部门过滤。
- 按状态过滤。
- 按标签过滤。
- 按创建时间倒序分页。
查询语句:
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)推荐索引:
db.data_asset.createIndex({
ownerDept: 1,
status: 1,
tags: 1,
createdAt: -1
})为什么复合索引顺序很重要:
| 字段 | 放在前面的原因 |
|---|---|
ownerDept | 等值过滤,能快速缩小部门范围 |
status | 等值过滤,继续缩小范围 |
tags | 数组匹配,多键索引支持标签筛选 |
createdAt | 支持范围分页和排序 |
如果把 createdAt 放最前面,而查询又主要按部门和状态筛选,索引会先扫一大段时间范围,再过滤部门,扫描量可能明显变大。
查询执行原理
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") 示例:
db.data_asset.find({
ownerDept: "信息科",
status: "PUBLISHED"
}).sort({ createdAt: -1 }).explain("executionStats")重点指标:
| 指标 | 解释 | 怎么判断 |
|---|---|---|
stage | 执行阶段 | 希望看到 IXSCAN,不要长期 COLLSCAN |
nReturned | 返回多少条 | 和扫描量对比 |
totalKeysExamined | 扫描多少索引键 | 过大说明索引选择不够精准 |
totalDocsExamined | 读取多少文档 | 远大于返回量说明过滤效率差 |
executionTimeMillis | 执行耗时 | 结合扫描量判断慢因 |
商业场景三:资产统计聚合
需求:按部门统计已发布资产数量和敏感资产数量。
db.data_asset.aggregate([
{ $match: { status: "PUBLISHED" } },
{
$group: {
_id: "$ownerDept",
total: { $sum: 1 },
sensitiveCount: {
$sum: {
$cond: [{ $eq: ["$securityLevel", "SENSITIVE"] }, 1, 0]
}
}
}
},
{ $sort: { total: -1 } }
])聚合管道不是一次性魔法,它是一段一段处理文档:
flowchart TD
A["原始文档集合"] --> B["$match 先过滤状态"]
B --> C["$group 按部门分组"]
C --> D["计算 total 和 sensitiveCount"]
D --> E["$sort 排序"]
E --> F["返回统计结果"]优化原则:
$match尽量放前面,先减少文档数量。$match字段尽量有索引。- 大聚合要关注内存和临时文件。
- 高频统计不要每次实时全量聚合,可以做预聚合表。
商业场景四:副本集高可用
副本集解决的是单节点故障问题。
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,让多数节点确认后再返回成功。
商业场景五:分片和热点
分片解决的是单副本集容量和写入吞吐上限。
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 支持多文档事务,但不能滥用。适合少量文档、短事务、必须原子一致的场景。
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()
}为什么事务不能乱用:
- 长事务会占用资源,影响并发。
- 分布式事务跨分片成本更高。
- 文档模型本来就鼓励把强一致的小对象聚合到单文档里。
- 如果大量业务都依赖复杂事务,说明可能更适合关系型数据库。
生产排查流程
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 核心全过程原理 |
| 基础 CRUD | MongoDB 基础 |
| 索引设计 | MongoDB 索引设计 |
| 聚合管道 | MongoDB 聚合管道 |
| 面试题 | MongoDB 面试 |
