MongoDB常见面试题总结(下)
MongoDB 面试的下半场聚焦两件事:查询怎么快(索引体系),容量怎么扩(分片集群)。线上事故也大多出在这两处——索引建错导致全集合扫描,分片键选错导致热点与迁移风暴。
考察地图
- 索引:索引的收益与代价、八种索引类型、复合索引字段顺序与最左前缀、TTL 过期机制、覆盖索引。考察的是查询优化的基本功。
- 分片:分片集群三种角色、分片键四条标准、范围与哈希分片的取舍、Chunk 分裂与均衡。考察的是水平扩展的机制理解。
- 建议自行查阅:
createIndex各选项参数、explain输出全部字段——工具类细节翻文档即可,面试重点在设计判断。
索引
索引为什么能加速查询?代价是什么?
结论:索引是有序的辅助数据结构,把全集合扫描变成树查找;代价是每次写入都要同步维护索引,拖慢写吞吐并占用内存与磁盘。
原理:没有索引时,MongoDB 只能做 collection scan——逐个文档比对条件,复杂度 O(N)。有合适索引时沿 B+ 树定位,检查的文档数从 N 降到接近 log N,还能直接借索引的有序性返回排序结果。写路径上,insert/update/delete 除了改文档本身,还要更新所有受影响的索引条目;同时索引常驻 WiredTiger 缓存,索引膨胀会挤出热数据。
延伸:写多读少的集合(如原始事件流)要克制建索引;验证索引是否生效的基本功是 explain(),关注执行 stage 是 IXSCAN 还是 COLLSCAN,以及 nReturned 与 totalDocsExamined 的比值。
追问:explain 里出现 SORT stage 说明什么?提示:排序没能下推给索引,需要内存排序,结果集超过 32MB 会直接报错。
MongoDB 有哪些索引类型?各自用在哪?
结论:常用的有单字段、复合、多键、哈希、文本、地理空间、唯一、TTL 八类,各自对应不同的查询形态。
原理:单字段索引建在单个字段上,排序方向无所谓——单字段索引可以正反向遍历。复合索引建在多个字段上,是业务查询的主力。多键索引针对数组字段,自动为数组每个元素建索引条目。哈希索引存字段值的哈希,专供哈希分片。文本索引支持字符串全文检索,一个集合至多一个,但性能一般,严肃搜索应交给 Elasticsearch。地理空间索引(2dsphere)处理经纬度查询。唯一索引是约束语义,保证字段值不重复。TTL 索引提供文档自动过期能力。
延伸:索引类型可以叠加语义——比如唯一复合索引、TTL 单字段索引;但多键字段之间有限制,一个复合索引最多只能包含一个数组字段。
追问:为什么复合索引里不能有两个数组字段?提示:避免两个数组的笛卡尔积式索引条目爆炸。
复合索引的字段顺序怎么定?
结论:复合索引按声明顺序先排第一字段、再在组内排第二字段;设计口诀是 ESR——等值(Equality)字段在前、排序(Sort)字段居中、范围(Range)字段最后。
原理:以 {userid: 1, score: -1} 为例,索引先按 userid 升序,同一 userid 内再按 score 降序。因此 sort({userid:1, score:-1}) 以及整体取反的 sort({userid:-1, score:1}) 都能走索引有序性;而 sort({userid:1, score:1}) 这种混合方向就不行。等值字段在前可以先把范围缩到最小,排序字段紧随其后借有序性免掉内存排序,范围条件放末尾是因为它只能过滤、无法再利用后续字段的有序性。
延伸:设计索引时把高频查询的过滤、排序、返回字段摆在一起看;验证排序是否下推,就看 explain 里还有没有 SORT stage。
追问:
sort({score:-1, userid:1})与索引{userid:1, score:-1}匹配吗?提示:不匹配,排序字段顺序必须与索引声明顺序一致。
复合索引遵循最左前缀吗?
结论:遵循。索引 {a,b,c} 等价于同时拥有 {a}、{a,b}、{a,b,c},但帮不了单独查 b 的查询。
原理:索引条目按 (a,b,c) 的字典序排列,定位必须从 a 开始;条件里没有 a 时,索引的有序性无从利用,只能走全索引扫描或全集合扫描。两个推论:已经建了 {a,b} 就不必再建冗余的 {a};当 a 上是范围条件时,后面的 b 只能用于过滤,不能再用于精确定位与排序。
延伸:这与 MySQL InnoDB 联合索引的最左前缀是同一原理——底层都是 B+ 树的字典序组织,一套理论两个数据库通用。
追问:
{a,b}索引下find({b:1})一定走不了索引吗?提示:若查询是覆盖查询且代价足够低,优化器可能选全索引扫描,但那不是 B+ 树定位,性质不同。
TTL 索引如何实现自动过期?
结论:在日期字段上建 TTL 索引并指定 expireAfterSeconds,后台线程周期性扫描并删除过期文档。
原理:mongod 的后台线程每 60 秒执行一轮清理,因此过期数据最长可能多保留约一分钟;删除量大时上一轮未清完,延迟还会叠加。副本集中只有主节点执行删除,从节点通过重放删除产生的 oplog 跟进。限制也要记牢:只能是单字段索引、字段必须是日期类型;_id 上不可建;capped collection 不支持;同一字段已有普通索引时不能再建 TTL 索引。
延伸:TTL 是「懒惰」清理,不保证精确的过期时刻——秒杀库存这类强时序语义别指望它;会话数据、日志保留周期才是它最舒服的场景。
追问:如果业务要求准点过期怎么办?提示:查询时显式加时间过滤条件,TTL 只作为兜底的物理清理。
什么是覆盖索引查询?
结论:当查询条件与返回字段全部落在同一个索引里时,MongoDB 无需回表取文档,直接从索引返回结果,这就是覆盖索引查询。
原理:索引条目比完整文档小且常驻内存,覆盖查询省掉了磁盘 I/O 与文档解析两步,是查询性能的「满血形态」。一个经典坑是投影里忘了排除 _id:索引条目里通常不含 _id(除非 _id 本身就是索引键),带着它就必须回表,覆盖随即失效。
延伸:配合 ESR 原则设计,列表页这类「过滤 + 排序 + 少字段返回」的查询经常可以做成覆盖索引,把整个查询压到纯索引路径上。
追问:覆盖索引与 MySQL 的覆盖索引是一回事吗?提示:原理相同——不回表;MongoDB 里等价说法是 covered query。
分片
分片集群由哪几部分组成?
结论:三种角色——mongos 负责路由,config server 保存集群元数据,shard 存储数据子集。
原理:mongos 是无状态的路由进程,从 config server 拉取 chunk 路由表,把请求定向到目标 shard 并汇总结果返回客户端。config server 自身部署为副本集,记录「哪个 chunk 在哪个 shard」这类元数据。每个 shard 自 3.6 起必须部署为副本集——分片解决扩展性,副本集解决高可用,两者正交组合成一个完整集群。
延伸:查询带上分片键时,mongos 能精确定向单个 shard(targeted query);不带分片键的查询只能广播到所有 shard 再汇总(scatter-gather),这是集群级慢查询的常见来源。
追问:mongos 挂了怎么办?提示:mongos 无状态,通常部署多个实例,由客户端驱动或负载均衡做故障切换。
分片键怎么选?四条标准是什么?
结论:高基数、分布均匀、匹配查询模式、避免单调——四条标准要同时掂量,选错代价极大。
原理:基数决定 chunk 的可拆分性:可选值太少(如性别),chunk 数量有限且容易长成无法拆分的 jumbo chunk,迁移不动,扩容随之失效。分布决定均衡:取值倾斜会把热点写死在某一个 shard 上。查询模式决定路由效率:常用查询带上分片键才能 targeted,否则全集群广播。单调性决定写入走向:单调递增键(如 ObjectId、时间戳)让新数据永远落在「最右」的 chunk,写入集中在单个 shard 并触发持续迁移,这种场景应改用哈希分片键打散。
延伸:4.2 之前分片键值不可修改,选错基本只能导数据重建;5.0 起支持 live resharding 可以在线换键,但成本高,前期评审仍是关键。
追问:分片键必须是索引吗?提示:是,分片键必须存在对应索引(单字段或复合索引前缀),且不能是多键、文本或地理索引。
范围分片与哈希分片怎么取舍?
结论:范围分片利于范围查询但可能产生写热点;哈希分片写入天然打散,但范围查询退化为全集群广播。
原理:范围分片按键值区间切分 chunk,相邻的键落在同一 shard,范围查询高效;但在单调键下新区间永远是最右侧 chunk,热点与迁移随之而来。哈希分片对键值取哈希后按哈希区间切分,写入随机散开、天然防热点;代价是相邻的原始键被打散到不同 shard,范围查询只能 scatter-gather。常见折中是复合分片键:低基数字段做前缀保证分区可拆分,再加一个字段做后缀把写入散开。
延伸:时序数据的经典做法是先按时间分桶(如按天生成 bucket 字段)再组合哈希后缀,既能让查询按时间裁剪,又避免写入全压在一个 chunk 上。
追问:哈希分片键能支持范围查询吗?提示:不能有效支持,范围条件必须广播到所有 shard。
Chunk 是什么?均衡器(Balancer)在干什么?
结论:chunk 是分片键一段连续区间内的文档集合,默认大小上限 64MB,是分片迁移的最小单位;balancer 负责在各 shard 之间搬运 chunk,让数量保持均衡。
原理:插入使 chunk 超过上限时触发分裂(split),一个变两个;各 shard 上 chunk 数量的差距达到阈值时,运行在 config server 主节点上的 balancer 启动迁移(moveChunk)。chunk 默认只分裂不合并——即使调大 chunkSize,已分裂的 chunk 也不会自动合回去。jumbo chunk 是分裂不了的超大 chunk:区间内分片键值无法再细分(如海量相同值),balancer 搬不动它,只能人工拆分或换键解决。
延伸:迁移消耗 I/O 与缓存,生产上通常设置均衡窗口放到业务低峰;新集群上线前做预分片(预先创建空 chunk 并分布到各 shard),能避免数据灌入初期的集中迁移风暴。
追问:为什么 chunk 是迁移的最小单位而不是单条文档?提示:元数据粒度与迁移成本的权衡——按文档记录位置,config server 的元数据会爆炸。

