MongoDB面试要点:从文档模型、索引到副本集和分片的系统梳理
前言
MongoDB 面试题通常不会只问“它是不是 NoSQL 数据库”。更常见的问题是:为什么用文档模型,文档怎么设计,索引怎么建,聚合怎么优化,副本集如何保证高可用,分片如何扩展容量,事务和关系型数据库有什么差异,慢查询应该怎么排查。
如果只把 MongoDB 理解成“可以存 JSON 的数据库”,回答很容易停留在表面。更好的方式是把它理解成一条完整链路:
1 | 应用写入文档 -> BSON 编码存储 -> WiredTiger 管理数据页和缓存 -> 索引定位数据 -> 副本集复制 oplog -> 分片集群按 shard key 路由请求 |
本文按面试中最常见的模块梳理 MongoDB 要点,适合作为面试前的复习清单。
MongoDB 整体架构
MongoDB 是一个面向文档的数据库,数据以集合和文档的形式组织。集合类似关系型数据库中的表,文档类似一行记录,但文档结构可以更灵活,字段可以嵌套对象和数组。
flowchart TD
A[客户端请求] --> B[mongod 实例]
B --> C[查询解析与执行]
C --> D[索引访问]
C --> E[WiredTiger 存储引擎]
E --> F[缓存与数据文件]
B --> G[oplog 复制日志]
G --> H[副本集成员]
面试可以这样概括:
MongoDB 的核心特点是文档模型、灵活 Schema、原生嵌套结构、丰富索引和水平扩展能力。单节点负责文档读写,副本集负责高可用和数据复制,分片集群负责把数据按 shard key 分散到多个分片上。
常见组件包括:
| 组件 | 作用 |
|---|---|
| mongod | MongoDB 数据库实例,负责数据存储、查询执行和复制 |
| mongos | 分片集群中的路由进程,负责把请求路由到正确分片 |
| config server | 保存分片集群元数据,例如 chunk 分布和路由信息 |
| replica set | 副本集,提供主从复制、故障转移和读扩展能力 |
| WiredTiger | 默认存储引擎,负责缓存、压缩、并发控制和持久化 |
文档模型怎么理解
MongoDB 使用 BSON 存储文档。BSON 可以理解为二进制形式的 JSON,但它支持更多数据类型,例如 ObjectId、Date、Decimal128、Binary 等。
文档模型的优势是:
| 优势 | 说明 |
|---|---|
| 贴近对象结构 | 复杂对象可以直接保存为嵌套文档,减少对象关系映射成本 |
| 读写局部性好 | 经常一起读取的数据可以放在同一个文档中,减少跨表关联 |
| Schema 灵活 | 不同行文档可以有不同字段,适合快速迭代的业务 |
| 数组和嵌套字段友好 | 标签、评论、地址、属性列表等结构可以自然表达 |
但文档模型不是随便把所有数据塞进一个大文档。设计时要看访问模式:
| 关系 | 建模建议 |
|---|---|
| 一对一 | 通常可以内嵌 |
| 一对少 | 通常可以内嵌,例如用户的几个地址 |
| 一对多且数量可控 | 可以内嵌数组,但要控制文档大小和更新频率 |
| 一对多且数量很大 | 通常拆成独立集合,通过引用关联 |
| 多对多 | 通常使用引用或中间集合 |
MongoDB 单个文档有大小限制,所以超大数组、高频追加日志、无限增长评论列表不适合全部内嵌到主文档中。
面试回答可以这样说:
MongoDB 建模要从查询模式出发。经常一起读、生命周期一致、数量可控的数据适合内嵌;数量无限增长、独立查询频繁、更新频繁或需要单独权限控制的数据更适合拆集合引用。
ObjectId 是什么
MongoDB 默认使用 _id 作为主键。如果插入文档时没有指定 _id,驱动通常会生成一个 ObjectId。
ObjectId 常见特点:
- 全局唯一概率很高,适合作为默认主键。
- 包含时间戳信息,大致按生成时间递增。
- 不需要数据库集中分配,适合分布式客户端生成。
- 不是严格连续自增 ID,不能当作强业务序号。
面试中经常会问:为什么 MongoDB 不默认用自增 ID?
可以回答:
自增 ID 在分布式环境下需要集中协调,容易形成写入热点和分配瓶颈。ObjectId 可以在客户端生成,避免集中分配,同时具备较好的唯一性和大致有序性。但如果业务需要可读、连续、有强含义的编号,应该单独设计业务编号字段。
WiredTiger 存储引擎
WiredTiger 是 MongoDB 的默认存储引擎。它负责把文档和索引持久化到磁盘,并通过缓存、压缩、检查点和日志机制提升性能与可靠性。
核心要点:
| 机制 | 说明 |
|---|---|
| Cache | 热数据和索引页优先放在内存缓存中 |
| Compression | 支持数据和索引压缩,降低磁盘占用 |
| Checkpoint | 周期性把内存中的一致性视图落盘 |
| Journal | 记录写入日志,用于异常宕机后的恢复 |
| MVCC | 通过多版本并发控制提升读写并发能力 |
| Document-level concurrency | 支持文档级并发控制,降低锁冲突 |
面试回答时不要只说“MongoDB 是内存数据库”。MongoDB 会利用内存缓存提升性能,但数据最终仍然持久化在磁盘中。
索引核心要点
MongoDB 的索引用来快速定位文档。没有合适索引时,查询通常会退化为集合扫描,也就是扫描大量文档后再过滤。
常见索引类型:
| 索引类型 | 适用场景 |
|---|---|
| 单字段索引 | 按某个字段精确查询或排序 |
| 复合索引 | 多字段组合查询、排序 |
| 多键索引 | 数组字段查询 |
| 唯一索引 | 保证字段唯一性 |
| TTL 索引 | 自动删除过期数据,例如会话、临时记录 |
| 文本索引 | 简单全文检索 |
| 地理空间索引 | 附近的人、门店位置、范围查询 |
| 部分索引 | 只为满足条件的文档建立索引,降低索引体积 |
| 稀疏索引 | 只索引包含该字段的文档 |
复合索引顺序
复合索引不是字段随便堆在一起。常见经验是 ESR 原则:
1 | Equality -> Sort -> Range |
例如常见查询:
1 | db.orders.find({ |
可以考虑:
1 | db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 }) |
但实际是否合适还要看字段区分度、数据量、查询频率和排序方式。
覆盖查询
如果查询需要的字段都能从索引中拿到,就不需要回表读取完整文档,这叫覆盖查询。
1 | db.users.find( |
如果存在 { age: 1, name: 1 } 这样的索引,就可能形成覆盖查询。
面试回答可以这样说:
MongoDB 索引优化的核心不是“字段都建索引”,而是围绕高频查询模式设计少量有效索引。复合索引要关注字段顺序、等值过滤、排序、范围查询、字段区分度和是否能覆盖查询。
explain 怎么看
MongoDB 可以通过 explain() 查看执行计划。
1 | db.orders.find({ userId: 1001 }).explain("executionStats") |
常见关注点:
| 字段 | 关注点 |
|---|---|
| winningPlan | 最终选择的执行计划 |
| stage | 是否出现 COLLSCAN、IXSCAN、FETCH、SORT 等阶段 |
| totalKeysExamined | 扫描了多少索引项 |
| totalDocsExamined | 扫描了多少文档 |
| nReturned | 返回了多少文档 |
| executionTimeMillis | 查询耗时 |
典型判断:
| 现象 | 可能问题 |
|---|---|
| COLLSCAN | 没有命中合适索引 |
| totalDocsExamined 远大于 nReturned | 过滤效率低,索引选择可能不合理 |
| 出现内存排序 | 排序字段没有被索引支持 |
| keysExamined 很大 | 范围过宽或索引区分度低 |
聚合管道
Aggregation Pipeline 是 MongoDB 做复杂分析和数据转换的核心能力。常见阶段包括:
| 阶段 | 作用 |
|---|---|
$match |
过滤文档 |
$project |
选择、重命名或计算字段 |
$group |
分组聚合 |
$sort |
排序 |
$limit / $skip |
限制或分页 |
$lookup |
跨集合关联 |
$unwind |
展开数组 |
$facet |
多路聚合 |
优化原则:
$match尽量前置,减少后续处理的数据量。$sort尽量利用索引,避免大规模内存排序。$project可以提前裁剪无用大字段。$lookup要控制关联集合规模,并为关联字段建立索引。- 大分页避免深度
$skip,可以用游标式分页。
面试回答:
聚合管道本质上是把数据处理拆成多个阶段。优化重点是尽早过滤、减少字段、让排序和关联尽量命中索引,避免在内存里处理过多文档。
事务与一致性
MongoDB 单文档写入天然具有原子性。也就是说,一个文档内部多个字段的更新要么都成功,要么都失败。
多文档事务适合需要跨多个文档或集合保持一致的场景,例如订单和账户余额同时更新。但事务不是 MongoDB 的首选建模手段。如果大量业务都依赖复杂跨文档事务,可能说明文档边界设计不合理,或者关系型数据库更适合该业务。
事务常见要点:
| 要点 | 说明 |
|---|---|
| 单文档原子性 | MongoDB 最基础、最常用的一致性能力 |
| 多文档事务 | 支持跨文档、跨集合操作保持一致 |
| 成本更高 | 事务会增加锁、日志、快照和重试成本 |
| 需要重试 | 分布式环境中事务可能因冲突、网络、选主而中断 |
面试表达:
MongoDB 支持事务,但最佳实践仍然是优先通过合理文档建模让一次业务修改尽量落在单文档内。只有确实需要跨文档一致性时,再使用多文档事务。
readConcern 与 writeConcern
MongoDB 的一致性语义经常通过 readConcern 和 writeConcern 表达。
| 参数 | 作用 |
|---|---|
| readConcern | 控制读到的数据满足什么一致性级别 |
| writeConcern | 控制写入需要被多少节点确认才算成功 |
| readPreference | 控制读请求发往主节点还是从节点 |
常见 writeConcern:
| 配置 | 含义 |
|---|---|
{ w: 1 } |
主节点确认写入即可返回 |
{ w: "majority" } |
多数节点确认后返回,可靠性更高 |
{ j: true } |
要求写入 journal 后再确认 |
面试中可以这样回答:
MongoDB 的写入可靠性和读取一致性不是固定不变的,可以通过 writeConcern、readConcern 和 readPreference 调整。可靠性越强,通常延迟越高;读从节点可以扩展读能力,但要考虑复制延迟和读到旧数据的问题。
副本集原理
副本集是 MongoDB 高可用的基础。一个副本集通常包含一个 primary 和多个 secondary。写请求默认进入 primary,primary 把写操作记录到 oplog,secondary 持续拉取并重放 oplog,从而保持数据同步。
flowchart LR
A[Client 写请求] --> B[Primary]
B --> C[(oplog)]
C --> D[Secondary 1]
C --> E[Secondary 2]
D --> F[可选读请求]
E --> F
关键点:
| 问题 | 回答重点 |
|---|---|
| primary 挂了怎么办 | 副本集会通过选举产生新的 primary |
| secondary 可以写吗 | 默认不能写,只能从 primary 复制数据 |
| oplog 是什么 | 记录写操作的有界集合,secondary 通过它复制数据 |
| 会不会丢数据 | 取决于 writeConcern,majority 写入比 w:1 更可靠 |
| 从节点读是否强一致 | 不一定,可能受复制延迟影响 |
面试表达:
副本集解决的是高可用和数据冗余问题。它通过 primary 写入、oplog 复制、secondary 重放和自动选举实现故障转移。生产环境关键写入通常会考虑 majority writeConcern。
分片集群
分片用于水平扩展数据容量和吞吐。它把一个集合的数据按照 shard key 拆分成多个 chunk,再分布到不同 shard 上。
flowchart TD
A[Client] --> B[mongos 路由]
B --> C[Config Server 元数据]
B --> D[Shard 1]
B --> E[Shard 2]
B --> F[Shard 3]
核心组件:
| 组件 | 作用 |
|---|---|
| mongos | 请求路由层,应用通常连接 mongos |
| config server | 保存分片元数据 |
| shard | 实际存储数据的分片,通常本身也是副本集 |
| shard key | 决定数据如何分布和请求如何路由 |
shard key 怎么选
shard key 是分片设计中最重要的选择之一。
好的 shard key 通常需要:
- 区分度高,能把数据打散。
- 查询中经常出现,方便定向路由。
- 写入分布均匀,避免集中写入某一个分片。
- 不容易变化,因为 shard key 更新成本高。
常见问题:
| shard key 问题 | 后果 |
|---|---|
| 低基数字段 | 数据集中到少数分片 |
| 单调递增字段 | 新写入集中到最后一个 chunk,形成热点 |
| 查询不带 shard key | mongos 需要广播到多个分片 |
| 后期才发现不合适 | 调整成本较高 |
面试回答:
分片不是简单加机器。关键在 shard key 设计。如果 shard key 不能支撑主要查询和写入分布,分片后反而可能出现广播查询、热点分片和迁移成本。
常见查询优化思路
MongoDB 慢查询排查可以按以下顺序:
- 看查询条件、排序字段和返回字段。
- 用
explain("executionStats")判断是否命中索引。 - 比较
totalDocsExamined、totalKeysExamined和nReturned。 - 检查是否有大文档、大数组、深分页或内存排序。
- 检查索引是否过多,写入是否被索引维护拖慢。
- 分片环境下检查查询是否带 shard key。
- 副本集环境下检查复制延迟、锁等待和磁盘 IO。
常见优化方式:
| 问题 | 优化方向 |
|---|---|
| 无索引扫描 | 为高频过滤字段建立合适索引 |
| 排序慢 | 建立能同时支持过滤和排序的复合索引 |
| 返回大文档 | 使用 projection 只返回需要字段 |
| 深分页慢 | 用基于 _id 或业务游标的分页 |
| 大数组更新慢 | 拆分集合或调整文档模型 |
$lookup 慢 |
控制关联规模,并为 foreignField 建索引 |
| 写入慢 | 减少无效索引,批量写入,检查 writeConcern |
MongoDB 和 MySQL 怎么选
面试中经常会问 MongoDB 与关系型数据库的差异。不要简单回答“MongoDB 快,MySQL 稳”。更准确的比较如下:
| 维度 | MongoDB | MySQL |
|---|---|---|
| 数据模型 | 文档模型,天然支持嵌套结构 | 表模型,结构化关系清晰 |
| Schema | 灵活,适合快速迭代 | 严格,适合强约束业务 |
| 关联查询 | 支持 $lookup,但不是主要优势 |
JOIN 成熟,适合复杂关系 |
| 事务 | 支持事务,但建模上优先单文档原子性 | 事务能力成熟,适合强一致核心交易 |
| 扩展 | 原生分片能力较强 | 通常依赖分库分表或中间件 |
| 典型场景 | 内容、日志、画像、配置、物联网、半结构化数据 | 订单、支付、账务、库存、强关系业务 |
面试表达:
MongoDB 适合文档结构自然、字段变化频繁、读写模式围绕聚合对象、需要水平扩展的场景。MySQL 更适合关系复杂、强约束、强事务、复杂 JOIN 和报表一致性要求高的场景。选型要看数据模型和访问模式,而不是只看性能。
高频面试题速记
| 问题 | 回答关键词 |
|---|---|
| MongoDB 为什么适合存复杂对象 | 文档模型、嵌套结构、数组、减少 ORM 映射 |
| BSON 和 JSON 有什么区别 | BSON 是二进制格式,支持更多类型,便于存储和遍历 |
_id 有什么作用 |
文档主键,默认唯一索引,常用 ObjectId |
| 索引是不是越多越好 | 不是,索引提升查询但增加写入和存储成本 |
| 复合索引顺序怎么定 | 等值、排序、范围,结合字段区分度和查询频率 |
| 什么是多键索引 | 数组字段上的索引,一个文档可能对应多个索引项 |
| 聚合管道怎么优化 | $match 前置、减少字段、排序用索引、控制 $lookup |
| MongoDB 支持事务吗 | 支持,但优先通过文档建模使用单文档原子性 |
| 副本集怎么同步 | primary 写 oplog,secondary 拉取并重放 |
| primary 故障怎么办 | 副本集选举新的 primary |
| 分片解决什么问题 | 水平扩展容量和吞吐 |
| shard key 怎么选 | 高基数、查询常用、写入均匀、稳定 |
| 从节点读有什么风险 | 可能读到旧数据,受复制延迟影响 |
| 慢查询怎么排查 | explain、索引、扫描量、排序、大文档、分片路由 |
总结
MongoDB 面试的主线可以归纳为四个层次:
- 数据模型:集合、文档、BSON、内嵌和引用如何取舍。
- 查询性能:索引、复合索引、覆盖查询、聚合管道和 explain。
- 可靠性:WiredTiger、journal、副本集、oplog、readConcern、writeConcern。
- 扩展性:分片集群、mongos、config server、chunk 和 shard key。
回答 MongoDB 问题时,最好始终围绕“业务访问模式”展开:数据怎么读、怎么写、是否一起返回、是否高频更新、是否需要强一致、是否需要水平扩展。能把文档模型、索引、副本集和分片串起来,基本就能覆盖大多数 MongoDB 面试追问。


