MongoDB 从零到生产级掌握
MongoDB 不能只理解成“能存 JSON 的数据库”。商业项目里真正要掌握的是:为什么文档模型能减少 join,什么时候嵌入什么时候引用,索引为什么能让查询变快,聚合管道怎么一步步处理文档,副本集怎么保证高可用,分片怎么扩展容量,事务为什么不能滥用,线上慢查询和数据倾斜怎么排查。
一句话建立主线:
MongoDB 是文档型数据库,用 BSON 文档组织数据,通过文档建模、索引、聚合、副本集、分片和事务能力支撑灵活结构和高吞吐读写;但它仍然需要围绕查询模式设计文档和索引。
如果你想检查自己是否真的从零基础学懂到能面试、能落地、能排查,按 MongoDB 从零到精通验收清单 逐项验收。
学习目标
学完这一页,你要能做到:
- 解释 database、collection、document、field、index 的关系。
- 解释 MongoDB 和 MySQL 的建模差异。
- 判断数据应该嵌入文档还是拆集合引用。
- 写基础 CRUD、条件查询、更新数组、分页和排序。
- 解释单字段索引、复合索引、多键索引、唯一索引、TTL 索引。
- 看懂
explain("executionStats")中的关键指标。 - 解释聚合管道
$match、$project、$group、$sort、$lookup的执行思路。 - 解释副本集 Primary、Secondary、oplog、选举、读写关注。
- 解释分片集群 shard key、mongos、config server、chunk 和数据倾斜。
- 解释 MongoDB 事务适用边界。
- 能排查慢查询、索引失效、文档过大、分片热点、复制延迟。
为什么需要文档数据库
关系库擅长结构稳定、关系明确、强事务的业务,例如订单、支付、库存、账务。MongoDB 更适合文档天然聚合、字段变化快、嵌套结构明显的业务。
| 场景 | 关系库做法 | MongoDB 做法 |
|---|---|---|
| 商品详情 | 商品表、图片表、属性表、规格表多表 join | 一个商品文档中嵌入详情和有限属性 |
| 用户画像 | 多张扩展表或 EAV 模型 | 一个用户画像文档保存灵活字段 |
| 埋点事件 | 固定宽表或日志表 | 事件文档保存不同事件字段 |
| 数据资产扩展属性 | 关系表频繁加字段 | 扩展属性放嵌套对象 |
MongoDB 的优势不是“不要设计”,而是“按访问模式设计文档”。不设计的 MongoDB 会比关系库更难维护。
MongoDB 核心结构
flowchart TD
A["Database"] --> B["Collection"]
B --> C["Document"]
C --> D["Field"]
B --> E["Index"]示例文档:
{
_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 的原因:
- 数据资产详情通常整体读取。
- 字段列表数量有限。
- 扩展字段可能变化。
- 标签、字段、敏感标识天然是嵌套结构。
嵌入还是引用
flowchart TD
A["要建模的数据"] --> B{"是否经常一起读取"}
B -- "是" --> C{"数量是否有限"}
C -- "是" --> D["优先嵌入"]
C -- "否" --> E["拆集合引用"]
B -- "否" --> E
E --> F["通过业务 ID 关联查询"]| 关系 | 推荐 | 原因 |
|---|---|---|
| 用户和少量地址 | 嵌入 | 一起读取,数量有限 |
| 商品和规格参数 | 嵌入 | 商品详情一起展示 |
| 用户和订单 | 引用 | 订单无限增长,生命周期不同 |
| 文章和评论 | 看规模 | 少量评论可嵌入,海量评论要拆 |
| 数据资产和字段定义 | 通常嵌入 | 字段定义随资产详情一起展示 |
| 数据资产和访问日志 | 引用 | 日志无限增长 |
如果把无限增长数组放进一个文档,会导致:
- 文档越来越大,接近 16MB 限制。
- 更新数组越来越慢。
- 单文档热点严重。
- 很难按数组元素做复杂分页。
CRUD Demo
插入:
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()
})查询:
db.data_asset.find({
ownerDept: "信息科",
status: "PUBLISHED",
tags: "医疗"
}, {
assetCode: 1,
name: 1,
ownerDept: 1,
createdAt: 1
}).sort({ createdAt: -1 }).limit(20)更新嵌套数组:
db.data_asset.updateOne(
{ assetCode: "DATASET_001", "fields.name": "patient_id" },
{ $set: { "fields.$.sensitive": true } }
)分页建议:
db.data_asset.find({
status: "PUBLISHED",
createdAt: { $lt: ISODate("2026-07-05T10:00:00Z") }
}).sort({ createdAt: -1 }).limit(20)大集合不建议深分页 skip 很大,因为前面的数据仍然要扫描跳过。更适合基于时间或 _id 的游标分页。
一次查询怎么执行
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 | 内存排序 | 可能需要排序索引 |
示例:
db.data_asset.find({
ownerDept: "信息科",
status: "PUBLISHED"
}).sort({ createdAt: -1 }).explain("executionStats")复合索引怎么设计
索引不是字段越多越好,而是匹配查询模式。
常见查询:
db.data_asset.find({
ownerDept: "信息科",
status: "PUBLISHED"
}).sort({ createdAt: -1 })索引:
db.data_asset.createIndex({
ownerDept: 1,
status: 1,
createdAt: -1
})为什么这样排:
ownerDept、status是等值过滤,放前面。createdAt用于排序,放后面。- 查询条件和排序方向能利用同一个复合索引。
常见索引类型:
| 索引 | 作用 | 场景 |
|---|---|---|
| 单字段索引 | 按一个字段查询 | assetCode |
| 复合索引 | 多条件过滤和排序 | ownerDept + status + createdAt |
| 多键索引 | 数组字段索引 | tags |
| 唯一索引 | 防重复 | assetCode |
| TTL 索引 | 自动过期 | 临时 token、短期日志 |
多键索引要注意数组字段过多会让索引膨胀,写入成本变高。
聚合管道全过程
聚合管道像流水线,一段一段处理文档。
flowchart TD
A["原始文档"] --> B["$match 过滤"]
B --> C["$project 裁剪字段"]
C --> D["$group 分组统计"]
D --> E["$sort 排序"]
E --> F["$limit 限制数量"]统计每个科室已发布资产数量:
db.data_asset.aggregate([
{ $match: { status: "PUBLISHED" } },
{ $group: { _id: "$ownerDept", total: { $sum: 1 } } },
{ $sort: { total: -1 } },
{ $limit: 20 }
])优化原则:
$match尽量放前面,减少后续文档数量。$project尽早裁剪无用字段。$sort尽量利用索引。$lookup要控制规模,关联字段建索引。- 大报表考虑预聚合,不要每次扫全量。
副本集原理
副本集解决高可用和数据冗余。
flowchart TD
A["Client 写入"] --> B["Primary"]
B --> C["写 oplog"]
C --> D["Secondary 1 拉取 oplog"]
C --> E["Secondary 2 拉取 oplog"]
D --> F["重放操作"]
E --> G["重放操作"]Primary 故障时会选举:
flowchart TD
A["Primary 故障"] --> B["Secondary 发现心跳异常"]
B --> C["发起选举"]
C --> D["多数节点投票"]
D --> E["新 Primary 产生"]
E --> F["客户端重新路由写入"]关键概念:
| 概念 | 含义 |
|---|---|
| Primary | 接收写入的主节点 |
| Secondary | 复制 oplog 的从节点 |
| oplog | 复制操作日志 |
| writeConcern | 写入确认级别 |
| readConcern | 读取一致性级别 |
| readPreference | 读主还是读从 |
如果读从节点,要接受复制延迟带来的短暂旧数据风险。
分片集群原理
分片解决单机容量和吞吐瓶颈。
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 很高 |
示例:
sh.shardCollection("asset_db.data_asset", { ownerDept: 1, assetCode: 1 })真实生产要根据数据量、查询条件、写入分布评估,不能随便拿自增 ID 当分片键。
MongoDB 事务边界
MongoDB 支持多文档事务,但它不是鼓励你把关系库模型照搬过来。
适合事务:
- 少量文档。
- 短事务。
- 明确业务边界。
- 偶尔需要跨集合一致。
不适合事务:
- 大批量长事务。
- 跨大量分片事务。
- 高频核心交易强一致链路。
- 本来可以通过文档嵌入解决的更新。
事务 Demo:
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 保存资产详情扩展:
- MySQL 保存核心主数据:资产 ID、编码、状态、权限。
- MongoDB 保存扩展详情:字段列表、标签、血缘、展示配置。
- Elasticsearch 保存搜索索引。
这样做的原因:
- 核心交易状态仍用关系库保证一致性。
- 灵活结构交给 MongoDB。
- 全文检索交给 ES。
- 不把一个数据库强行用于所有场景。
埋点和日志事件
事件字段经常变化,MongoDB 可以保存原始事件文档。但查询必须设计好索引,例如:
db.event_log.createIndex({ eventType: 1, createdAt: -1 })
db.event_log.createIndex({ userId: 1, createdAt: -1 })如果只写不管,集合会快速膨胀。要考虑归档、TTL、冷热分层和聚合统计。
线上排查流程
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"]常用命令:
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 适合什么场景
MongoDB 适合文档天然聚合、字段变化较快、嵌套结构明显、读写吞吐较高的场景,比如商品详情、用户画像、内容草稿、日志事件、数据资产扩展属性。它不适合大量复杂 join、强事务和高度关系化的核心交易场景。MongoDB 不是随便存 JSON,仍要按查询模式设计文档和索引。嵌入和引用怎么选
经常一起读取、生命周期接近、数量有限的数据适合嵌入;不经常一起读取、数量无限增长、生命周期不同的数据适合拆集合引用。比如用户地址可以嵌入,用户订单不适合无限追加到用户文档里。MongoDB 查询为什么快
MongoDB 查询快主要依赖合适的索引和合理的文档模型。有索引时可以通过 B-Tree 索引快速定位候选文档,再 FETCH 文档;如果经常一起读取的数据被嵌入在同一文档里,也能减少关系库多表 join 的成本。但没有合适索引时同样会 COLLSCAN,性能会很差。副本集和分片区别
副本集解决高可用和数据冗余,Primary 写入,Secondary 复制 oplog,Primary 故障后选举新主。分片解决容量和吞吐扩展,通过 shard key 把数据分布到多个 shard。副本集关注可用性,分片关注水平扩展。关联知识点
| 知识点 | 继续学习 |
|---|---|
| MongoDB 总览 | 开始 |
| 从零到精通验收 | MongoDB 从零到精通验收清单 |
| 商业场景训练营 | MongoDB 商业场景训练营 |
| MongoDB 基础 | MongoDB基础 |
| 核心全过程原理 | 核心全过程原理 |
| 索引设计 | 索引设计 |
| 聚合管道 | 聚合管道 |
| MongoDB 高级 | MongoDB高级 |
| MongoDB 面试 | MongoDB面试 |
| MySQL | MySQL 从零到生产级掌握 |
| Elasticsearch | ES 从零到生产级掌握 |
本章小结
MongoDB 学到生产级,重点不是会写 insertOne 和 find,而是能根据查询模式设计文档,能用索引支撑过滤和排序,能用聚合管道处理统计,能理解副本集和分片的边界,能知道事务为什么不能滥用,并且能用 explain、oplog、分片状态和监控指标定位线上问题。
