MongoDB(一):基础概念、文档模型与存储引擎
MongoDB(一):基础概念、文档模型与存储引擎
导语:MongoDB 是文档型 NoSQL 的代表,面试通常从「和 MySQL 有什么区别、什么时候该用它」切入,再深挖文档模型(BSON、ObjectId、内嵌 vs 引用)与存储引擎(WiredTiger、文档级并发、checkpoint 与 journal)。本篇覆盖基础概念、数据建模、CRUD 高频考点与存储引擎原理,共 20 题。
一、MongoDB 与关系型数据库
1. MongoDB 是什么?和 MySQL 有什么区别?(必问)
答: MongoDB 是一个面向文档(Document-Oriented)的分布式 NoSQL 数据库,用类 JSON 的 BSON 存储数据,具备灵活的 Schema、水平扩展能力与原生高可用。
| 维度 | MySQL | MongoDB |
|---|---|---|
| 数据模型 | 关系模型,表 + 行 + 列,Schema 固定 | 文档模型,集合 + 文档,Schema 灵活(同一集合的文档字段可以不同) |
| 存储格式 | 行存(按行组织,适合 OLTP 点查) | BSON 文档(可整体读写,适合对象整体取出) |
| 关联 | JOIN 是核心能力 | 无传统 join,靠内嵌文档或 $lookup(性能不如 MySQL join) |
| 事务 | 强事务,成熟 | 4.0 起支持多文档事务(副本集),4.2 起支持分片事务;单文档操作天生原子 |
| 扩展方式 | 主要靠垂直扩展 + 分库分表(应用层改造大) | 原生分片集群,水平扩展是内建能力 |
| 索引 | B+Tree、聚簇索引、覆盖索引 | B 树族索引(WiredTiger)、多键索引、文本/地理/通配符等 |
| 一致性 | 强一致(默认) | 可调(readConcern / writeConcern),默认最终一致语义可配置为强一致 |
| 典型场景 | 交易、财务、强关系业务 | 内容管理、日志、IoT、商品详情、用户画像、实时分析 |
一句话总结:MySQL 强在「关系与事务」,MongoDB 强在「灵活模型与水平扩展」。二者是互补关系,不是替代关系。
2. MongoDB 的优点和缺点是什么?
答:
优点
- Schema 灵活:需求频繁变更(字段增减、结构不一致)时不需要 DDL 迁移;
- 读写性能好:内嵌文档一次读取出完整对象,避免多表 join;
- 水平扩展原生:分片集群 + 副本集,扩容相对平滑;
- 开发效率高:JSON 文档与主流语言的 POJO 天然对应,无阻抗失配;
- 功能丰富:聚合管道、地理索引、全文索引、TTL 过期、Change Streams、时序集合、可查询加密。
缺点
- 不支持强关系约束:没有外键,一致性靠应用保证;
- 多文档事务较「重」:性能弱于 MySQL,默认 60 秒超时,不适合长事务;
- 内存敏感:工作集(索引 + 热点数据)应放得进内存,否则磁盘 IO 拖垮性能;
- 文档大小限制 16MB:不适合存储超大对象(需用 GridFS);
- 数据冗余:内嵌模型容易产生冗余,更新需要同步多份。
3. MongoDB 适合哪些场景?什么时候不该用?
答:
适合
- 对象整体读写:商品详情(SKU + 属性 + 图文)、订单快照、文章与评论;
- 日志/事件/埋点:写入量大、模型不固定、按时间查询(配合时间序列集合);
- IoT 与监控数据:设备上报字段差异大,TTL 自动清理;
- 用户画像与标签:数组、嵌套结构天然适配;
- 内容管理与 CMS:字段频繁调整。
不适合
- 强事务的金融核心(转账、账务):MySQL / 分布式事务中间件更合适;
- 复杂多表关联分析:适合关系型或 OLAP 数仓;
- 数据量小且关系简单:用 MySQL 更省心;
- 需要复杂 SQL 与存储过程的场景。
4. NoSQL 有哪些分类?MongoDB 属于哪一类?
答:
| 类型 | 代表 | 数据模型 | 典型场景 |
|---|---|---|---|
| 文档型 | MongoDB、CouchDB | JSON/BSON 文档 | 内容、商品、用户画像 |
| 键值型 | Redis、Memcached | K-V | 缓存、会话、计数器 |
| 列式 | HBase、Cassandra | 列族 | 海量写入、时序 |
| 图 | Neo4j | 节点 + 边 | 社交关系、风控、推荐 |
MongoDB 属于文档型,同时兼具键值型的简单性与部分关系型的能力(二级索引、聚合、事务)。
二、核心概念与数据类型
5. MongoDB 的核心概念与 MySQL 如何对应?
答:
| MongoDB | MySQL | 说明 |
|---|---|---|
| Database(数据库) | Database | 一个实例可有多个库 |
| Collection(集合) | Table | 无固定 Schema |
| Document(文档) | Row | 一个 BSON 对象,最大 16MB |
| Field(字段) | Column | 文档内的键值 |
| Index(索引) | Index | 默认 _id 索引 |
| Embedded Document | JOIN 的结果 | 用嵌套替代关联 |
| mongos / mongod | mysqld | 路由进程 / 数据进程 |
注意:MongoDB 没有「行、列、表」的概念,考试如果把文档说成「行」、集合说成「表」只能算类比,准确表述是「文档与集合」。
6. BSON 是什么?和 JSON 有什么区别?(高频)
答: BSON = Binary JSON,是 MongoDB 的底层存储与网络传输格式。
| 维度 | JSON | BSON |
|---|---|---|
| 形式 | 文本 | 二进制 |
| 解析 | 需解析文本,较慢 | 长度前缀 + 类型标记,可高效遍历与跳读 |
| 数据类型 | 只有 string/number/boolean/null/array/object | 额外支持 ObjectId、Date、Timestamp、Binary、Decimal128、正则、MinKey/MaxKey、Int32/Int64/Double |
| 空间 | 更省 | 略大(带类型与长度信息,换取解析速度) |
| 有序性 | 对象键无序 | 保留字段顺序(影响比较与索引排序) |
// BSON 特有的类型(JSON 无法直接表达)
db.users.insertOne({
_id: ObjectId("65f1c2a4b8e1a2f3d4c5b6a7"),
name: "张三",
birthday: ISODate("1995-05-20T00:00:00Z"), // Date
balance: NumberDecimal("99.99"), // 高精度十进制,避免 double 误差
avatar: BinData(0, "aGVsbG8="), // 二进制
tags: ["java", "mongodb"]
})精度考点:金额不要用 double(二进制浮点有舍入误差),用 Decimal128(
NumberDecimal)。
7. ObjectId 的结构是什么?怎么从 _id 反推创建时间?(高频)
答: _id 是文档的默认主键,若插入时未指定,驱动会自动生成 ObjectId(12 字节):
| 组成 | 长度 | 说明 |
|---|---|---|
| 时间戳 | 4 字节 | Unix 时间戳(秒级) |
| 随机值 | 5 字节 | 进程级随机数,保证多机/多进程不冲突 |
| 计数器 | 3 字节 | 进程内自增,同一秒内可产生 1,677 万+ 个不重复 ID |
const id = ObjectId("65f1c2a4b8e1a2f3d4c5b6a7");
id.getTimestamp() // ISODate("2024-03-13T..."):直接拿到创建时间
id.str // 字符串形式
id.toString() // 24 位十六进制
ObjectId.isValid("65f1c2a4b8e1a2f3d4c5b6a7") // true要点与坑:
- 近似有序:时间戳在前,所以 ObjectId 大致按插入时间递增,可以据此做范围分页(
_id > lastId),但秒级精度意味着同一秒内靠计数器区分。 - 不是全局严格单调递增:不同机器/进程的随机部分不同,跨机器不保证顺序,因此用 ObjectId 做分片键依然可能热点(接近单调递增)。
_id一旦写入不可修改;_id索引默认唯一且不能删除。- 业务可以用自己的主键(字符串、数字),但不要用无上限的自增数字做分片键(见分片篇)。
8. 常用数据类型有哪些?
答: 字符串、整型(Int32/Int64)、Double、Decimal128、Boolean、Date、Timestamp(内部使用,用于 oplog)、ObjectId、Array、Embedded Document、Binary、Null、正则、MinKey/MaxKey。
两个高频注意事项:
- 类型敏感:MongoDB 严格按类型比较,
{"age": 18}(Int32)与{"age": "18"}(String)不会互相匹配,查询时必须传对类型,驱动映射错误会直接导致索引失效、全表扫描。 - 字段顺序:BSON 保持字段顺序,甚至可以用字段顺序做查询条件(
{"a": 1, "b": 1}与{"b": 1, "a": 1}是不同文档),但业务上不要依赖这一点。
9. 文档大小有限制吗?超过 16MB 怎么办?
答: 单个 BSON 文档最大 16MB(硬限制,无法调整),这条限制还衍生出几个相关问题:
- 无界数组是经典事故:往数组里不断
$push评论、日志,最终触顶报错。解决方式是把数组拆成独立集合(引用模型)。 - 超大文件用 GridFS:把一个文件切成多个 255KB 的 chunk 存到
fs.chunks,元数据存fs.files,支持断点续传与流式读取。
// 用 mongofiles 上传/下载(GridFS)
// mongofiles -d files put ./big.zip
// mongofiles -d files get big.zip- 集合名长度:未分片集合上限 255 字节,分片集合上限 235 字节(命名空间预算所限)。
- 索引数量上限:每个集合最多 64 个索引;复合索引最多 32 个字段;分片键最大 512 字节。
三、数据建模(内嵌 vs 引用)
10. 内嵌文档和引用有什么区别?如何选择?(必问)
答: 这是 MongoDB 数据建模的第一问题,本质是「冗余换读性能」还是「规范换一致性」。
| 维度 | 内嵌(Embedded) | 引用(Reference) |
|---|---|---|
| 存储 | 子文档放在父文档内 | 只存 _id,另建集合 |
| 读性能 | 一次查询拿到全部 | 需要二次查询或 $lookup |
| 写性能 | 更新内嵌字段较麻烦,文档膨胀 | 更新独立、可控 |
| 一致性 | 冗余数据需同步更新 | 天然单一数据源 |
| 大小限制 | 父文档 16MB 上限 | 无此问题 |
| 适用 | 一对一、一对少、读多写少、需要原子更新 | 一对多且「多」很大、多对多、被共享的数据 |
选择原则:
- 数据访问模式优先:如果 90% 的查询都需要订单 + 订单明细一起返回,就内嵌;
- 一对少(few)内嵌,一对多(many)引用:博客的「作者信息」内嵌,但「文章列表」用引用;
- 数组会无限增长时必须拆出去(评论、日志、消息);
- 被多处引用的公共数据用引用(用户、商品、字典);
- 需要原子更新一组数据时优先内嵌(单文档操作原子,无需事务)。
// 内嵌:订单 + 明细(总是一起读,且明细数量有限)
db.orders.insertOne({
_id: "order_1001",
userId: ObjectId("65f1c2a4b8e1a2f3d4c5b6a7"),
status: "PAID",
items: [
{ sku: "A-1", name: "键盘", price: NumberDecimal("299.00"), qty: 1 },
{ sku: "B-2", name: "鼠标", price: NumberDecimal("99.00"), qty: 2 }
],
createdAt: new Date()
})
// 引用:文章 -> 作者(作者信息会被多处引用,且可能变更)
db.articles.insertOne({
_id: "article_2001",
title: "MongoDB 面试精讲",
authorId: ObjectId("65f1c2a4b8e1a2f3d4c5b6a7"), // 只存引用
tags: ["database", "mongodb"],
createdAt: new Date()
})11. 一对多、多对多应该怎么建模?
答:
- 一对一:直接内嵌(如用户 + 用户详情)。避免拆表,因为 MongoDB 没有 join 优势。
- 一对多:
- 一对少(如文章 → 评论 ≤ 几十条):把「多」内嵌到「一」;
- 一对多但数量大(如商家 → 商品 上万):用「子文档存父引用」——把
merchantId放在商品文档(属于「多」的一方),并建立merchantId索引; - 极端「多」的场景(如订单 → 明细 上千条):拆成两个集合并用
orderId关联。
- 多对多:
- 双向引用:两端各存对方
_id数组(适合两端数量都不大,如文章 ↔ 标签); - 中间集合:像关系型一样建关联集合(适合关联本身带属性,如「用户 - 课程」带报名时间、成绩)。
- 双向引用:两端各存对方
// 一对多:多的一方存引用 + 索引
db.products.createIndex({ merchantId: 1, createdAt: -1 }) // 支撑「按商家分页查商品」
db.products.insertOne({ _id: "P-1", merchantId: "M-88", name: "键盘", price: 299 })
// 多对多:中间集合
db.enrollments.insertOne({ userId: "U-1", courseId: "C-9", enrolledAt: new Date(), score: 92 })
db.enrollments.createIndex({ userId: 1, courseId: 1 }, { unique: true })12. Schema 灵活是不是意味着可以随便设计?
答: 不是。MongoDB 4.4 起支持 $jsonSchema 校验,可以在集合上强制字段类型与必填项;工程上更推荐用应用层 DTO + 校验注解统一约束,避免「同一个集合里五花八门」导致索引与查询混乱。
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "age"],
properties: {
name: { bsonType: "string", description: "必填且必须是字符串" },
age: { bsonType: "int", minimum: 0, maximum: 150 },
tags: { bsonType: "array", items: { bsonType: "string" } }
}
}
},
validationLevel: "strict", // strict:插入与更新都校验
validationAction: "error" // error:拒绝;warn:仅记录日志
})四、CRUD 高频考点
13. insertMany 的 ordered 参数有什么影响?
答:
ordered: true(默认):顺序插入,遇错即停,后续文档不再插入;ordered: false:并行、无序插入,单条失败不影响其它,能显著提升批量写入吞吐,适合导入数据。
try {
db.users.insertMany(docs, { ordered: false })
} catch (e) {
print(e.writeErrors.length + " 条失败,其余已成功写入")
}生产建议:批量导入用
ordered: false;同时注意单批次大小不要过大(驱动会按maxWriteBatchSize自动切分,但过大的批次会占用内存与网络)。
14. updateOne 和 replaceOne 有什么区别?$set 与整对象替换呢?
答:
updateOne+ 更新操作符($set/$inc…):局部修改,其它字段保留(推荐);replaceOne:用新文档整体替换(除_id外所有字段被删掉并重建);updateOne不写操作符(如updateOne(filter, {name:"x"})):MongoDB 会报错,必须使用操作符——这是与replaceOne的经典区别。
// 局部更新:只改 status,其它字段保留
db.orders.updateOne({ _id: "order_1001" }, { $set: { status: "SHIPPED" } })
// 整体替换:除 _id 外全部覆盖(慎用,容易丢字段)
db.orders.replaceOne({ _id: "order_1001" }, { status: "SHIPPED" })
// upsert:不存在则插入,存在则更新(返回 upsertedId 可判断)
db.counters.updateOne(
{ _id: "orderSeq" },
{ $inc: { seq: 1 } },
{ upsert: true }
)常用更新操作符速查:
| 操作符 | 作用 |
|---|---|
$set / $unset | 设置 / 删除字段 |
$inc / $mul | 数值自增 / 自乘 |
$min / $max | 取更小 / 更大值 |
$push / $addToSet / $pull / $pop | 数组追加(可重复)/ 去重追加 / 删除匹配元素 / 首尾弹出 |
$rename | 字段重命名 |
$currentDate | 写入当前时间 |
$setOnInsert | 仅 upsert 插入时生效 |
15. 单文档操作是原子的吗?(高频)
答: 是。 MongoDB 保证单个文档的写入操作是原子性的(即使不带事务):一条 updateOne 里同时 $set 多个字段、$inc 多个计数器,要么全成功要么全失败,不会出现「改了一半」。
由此衍生出两条重要设计经验:
- 多个字段需要「一起正确」时,把它们放进同一个文档,就不需要事务;
- 跨多个文档的一致性才需要 4.0+ 的多文档事务(见副本集篇)。
// 原子扣库存 + 记录流水(同一文档内,无需事务)
db.inventory.updateOne(
{ sku: "A-1", stock: { $gte: 2 } }, // 条件里带上库存判断,防超卖
{ $inc: { stock: -2 }, $push: { logs: { at: new Date(), delta: -2 } } }
)16. bulkWrite 与 find 的常用写法?count 该怎么用?
答:
// bulkWrite:一次网络往返执行多种写入(默认 ordered: true)
db.users.bulkWrite([
{ insertOne: { document: { _id: 1, name: "张三" } } },
{ updateOne: { filter: { _id: 2 }, update: { $set: { vip: true } }, upsert: true } },
{ deleteOne: { filter: { _id: 3 } } }
], { ordered: false })// 查询:投影 + 排序 + 分页(只取需要的字段,减少网络与内存开销)
db.orders.find({ status: "PAID" }, { _id: 0, orderNo: 1, amount: 1 })
.sort({ createdAt: -1 })
.skip(20).limit(10)
// 计数三兄弟
db.orders.countDocuments({ status: "PAID" }) // 准确计数(会扫描/走索引,推荐)
db.orders.estimatedDocumentCount() // 读元数据,极快但可能不准(分片/异常关闭后偏差)
// db.orders.count() // 已弃用,不要再用面试延伸:深度分页(
skip(100000))不管在 MySQL 还是 MongoDB 都是灾难,skip需要扫描并丢弃前面的文档。MongoDB 的优化方式是带上上一页的游标值做范围查询:
// 基于 _id 的游标分页(利用 _id 索引,稳定且快)
db.orders.find({ _id: { $gt: lastId }, status: "PAID" })
.sort({ _id: 1 }).limit(10)五、存储引擎与内存结构
17. WiredTiger 是什么?为什么 MongoDB 从 MMAPv1 换成了它?
答: WiredTiger 是 MongoDB 3.2 起的默认存储引擎,替换了 MMAPv1。核心能力:
| 能力 | 说明 |
|---|---|
| 文档级并发控制 | MMAPv1 是库级/集合级锁,WiredTiger 锁到文档(乐观并发 + 写冲突重试),并发写能力大幅提升 |
| MVCC 与快照 | 多版本并发控制,读操作看到一致性快照,读写互不阻塞 |
| 数据压缩 | 支持 snappy(默认)、zstd、zlib、无压缩,显著降低磁盘占用 |
| checkpoint + journal | 定期检查点 + 预写日志,崩溃后可恢复 |
| B 树索引 + 有序存储 | 支持游标、范围扫描与高效的内存缓存 |
// 查看存储引擎与压缩配置
db.serverStatus().storageEngine // { name: "wiredTiger", ... }
db.getCollection("orders").stats().wiredTiger // 集合级块与压缩信息
// 创建集合时指定压缩算法(zstd 压缩率更高,CPU 开销略大)
db.createCollection("logs", { storageEngine: { wiredTiger: { configString: "block_compressor=zstd" } } })18. MongoDB 的索引到底是 B 树还是 B+ 树?(争议题,会答就是加分)
答: 这道题没有「标准官方答案」,正确的回答方式是说清事实与争议:
- 官方口径:WiredTiger 文档与源码中一律称其为 B-Tree(
btree模块),MongoDB 官方文档也写 "B-tree"; - 实现细节:WiredTiger 的实现具备 B+ 树的典型特征——内部节点只存键与子页指针,数据与 RecordId 存放在叶子页,因此很多资料(包括 WiredTiger 作者的分享)称其为 B+ 树;
- 与 MySQL 的本质差异不在「B 树还是 B+ 树」的字面之争,而在索引与数据的组织方式:
| 维度 | MySQL InnoDB | MongoDB WiredTiger |
|---|---|---|
| 树的称呼 | B+Tree(明确) | B-Tree(官方)/B+ 树(实现) |
| 数据与索引 | 聚簇索引:主键索引叶子节点直接存整行数据 | 索引与文档分离:索引叶子节点存 RecordId,需回表到文档 |
| 二级索引 | 叶子存主键,需回表(或覆盖索引) | 叶子存 RecordId,需回表 |
| 页内存储 | 数据页 16KB | WiredTiger page(默认内部页/叶子页按压缩块组织) |
面试建议表述:
「WiredTiger 官方把它叫做 B-Tree,但实现上内部节点不存数据、数据都在叶子页,具备 B+ 树的特征,所以资料里两种说法都有。真正值得说的是:MySQL 主键索引是聚簇索引,B+ 树叶子节点直接存整行;而 MongoDB 的索引叶子节点存的是 RecordId,取文档需要回表,这也是为什么 MongoDB 强调覆盖查询(covered query)能省一次随机 IO。」
19. 一次写入 MongoDB 经历了什么?数据会丢吗?(高频)
答: 写入链路分四步,理解它就能回答「数据安全性」:
- 写入 WiredTiger cache(内存中的 B 树页),同时记录操作日志到 journal(WAL);
- 写 oplog(副本集),从节点拉取重放;
- journal 刷盘:默认每 100ms(
journalCommitInterval,可设 1~500ms)将日志刷到磁盘;如果写关注带j: true,则强制等待 journal 落盘才返回确认; - checkpoint:默认每 60 秒(
checkpoint=(wait=60, log_size=2GB),满足任一条件触发)把内存中的脏页刷成磁盘上的一致性快照,并截断已无用的 journal。
// 强一致写入:等待 journal 落盘 + 多数成员确认
db.orders.insertOne(
{ orderNo: "1001", amount: 99 },
{ writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
)结论:
- 不丢的前提是写关注到位。
{ w: 1 }(默认)在主节点宕机且未复制出去的瞬间可能丢数据;{ w: "majority", j: true }才是金融级要求。 - 崩溃恢复:进程崩溃后从 journal 重放未落盘的变更;checkpoint 保证不需要从头重放。
- MongoDB 8.0 的变化:使用
w: "majority"的写操作在多数成员写入 oplog 后即返回确认(此前要等多数成员「应用」变更完成)。这提升了 majority 写的性能,但代价是写后立刻从从节点做非因果一致性读,可能读不到这次写入——需要用因果一致会话(causal session)或readConcern: majority兜住。
20. MongoDB 的内存是怎么用的?WiredTiger cache 默认多大?
答:
- WiredTiger cache:默认大小为
max(50% × (物理内存 - 1GB), 256MB),可通过storage.wiredTiger.engineConfig.cacheSizeGB调整。它缓存的是解压后的 B 树页。 - 文件系统缓存(OS page cache):数据文件同样被操作系统缓存,所以 MongoDB 进程 RSS 高于 cache 上限是正常的。
- 核心指标是「工作集(Working Set)」:索引 + 热点数据应能放进内存。放不下就会出现大量 page fault、磁盘 IO 飙升、查询变慢。
// 关键内存与缓存指标
db.serverStatus().wiredTiger.cache
// 关注:bytes currently in the cache(当前缓存占用)
// tracked dirty bytes in the cache(脏页字节数,长期偏高说明刷盘跟不上)
// pages read into cache / pages written from cache
db.serverStatus().extra_info.page_faults // 页错误次数,持续增长说明内存不足
// 调整 cache(配置文件中设置,需重启)
// storage:
// wiredTiger:
// engineConfig:
// cacheSizeGB: 8调优经验:
- 容器里部署时务必显式设置
cacheSizeGB,否则 MongoDB 按宿主机内存计算,容易 OOM 被杀; - 把
cacheSizeGB设为「可用内存的 50% ~ 60%」,剩下的留给 OS page cache 与连接开销; - 内存不足先优化索引与查询(减少扫描),再考虑加内存或分片。
21. MongoDB 的锁机制是怎样的?
答: WiredTiger 使用多粒度意向锁 + 文档级并发控制:
| 锁级别 | 说明 |
|---|---|
| Global(全局) | 如创建/删除数据库、replSetStepDown 等 |
| Database(库级) | 如创建/删除集合、dropDatabase |
| Collection(集合级) | 如重建索引、drop 集合 |
| Document(文档级) | 真正的数据读写,WiredTiger 用乐观并发:先无锁写,冲突时抛 WriteConflict 并重试 |
关键点:
- 读写不互斥:MVCC 让读看到快照,不阻塞写;两个写同一文档才会冲突;
WriteConflict是可重试错误:事务中遇到它会返回TransientTransactionError,应用应重试整个事务;- 常见「锁等待」表现为慢查询而非直接报错,用
db.currentOp()/$currentOp定位。
// 查看当前正在执行的操作(找长时间运行或等待锁的请求)
db.adminCommand({ currentOp: 1, "active": true, "secs_running": { $gte: 3 } })
// 杀掉超时操作
db.adminCommand({ killOp: 1, op: <opid> })六、版本演进与常见坑
22. MongoDB 各版本有哪些里程碑特性?(面试可延伸)
答:
| 版本 | 里程碑能力 |
|---|---|
| 3.2 | WiredTiger 成为默认引擎、$lookup 关联查询 |
| 3.6 | Change Streams(变更流)、$jsonSchema 校验 |
| 4.0 | 副本集多文档事务、$convert 等聚合增强 |
| 4.2 | 分片集群事务、通配符索引、$merge |
| 4.4 | 隐藏索引(hideIndex)、$unionWith、refineCollectionShardKey、$function/$accumulator(为替代 MapReduce 铺路) |
| 5.0 | 时间序列集合、在线重分片(reshardCollection)、$setWindowFields 窗口函数 |
| 6.0 | 分片数据分布与均衡优化、时序集合压缩增强、集群内同步 |
| 7.0 | 可查询加密 GA、分片元数据重构(每个分片维护自己的配置集合)、$search 生态集成 |
| 8.0 | 性能大版本:读吞吐最高 +36%、并发写 +20%、新 bulkWrite(可跨集合)快 56%、时序块处理提升超过 200%;moveCollection/unshardCollection(集合可移动、可取消分片)、配置分片(Config Shard)、majority 写确认时机优化 |
| 8.1 / 8.2 | 快速发布版(Rapid Release),仅 Atlas 支持,本地部署不提供 |
| 8.3 | 当前稳定版(2026-05 GA),延续性能与安全增强 |
支持周期:GA 主版本自发布起支持 30 个月。当前(2026 年 9 月)仍在支持期的版本是 8.3 / 8.0 / 7.0,其中 7.0 将在 2027-08 结束支持;6.0 及更早版本已 EOL。
23. MongoDB 有哪些「一踩一个准」的常见坑?
答:
- 无界数组:往文档里无限
$push,最终撞上 16MB 上限导致写入失败。→ 拆集合。 - 类型不一致:
age有的存 Int 有的存 String,导致索引无法有效使用、查询漏数据。→$jsonSchema或应用层约束。 skip深度分页:skip(100000)扫描并丢弃大量文档。→ 游标(range)分页。- 无索引的
$lookup:foreignField没索引时,每次关联都全表扫描。→ 必须先给它建索引。 $or+ 部分字段无索引:只要$or的分支里有未索引字段,就可能退化为全集合扫描。- 正则不带
^:{$regex: "abc"}无法走索引,只有前缀匹配(/^abc/、区分大小写、非i选项)才能利用索引。 count()与countDocuments()混用:前者已废弃,后者走查询(准确但慢),统计总量用estimatedDocumentCount()。- 容器内存未限制:
cacheSizeGB默认按宿主机算,容器内极易 OOM。 w:0(fire-and-forget)滥用:写入不确认会把缓冲压力转成内存增长,量大时反而拖垮实例。- 把 MongoDB 当缓存用:缺乏
remove语义的自动过期机制时,需用 TTL 索引,否则集合会无限膨胀。
// TTL 索引:让文档在指定时间后自动过期(后台每 60 秒扫描一次)
db.sessions.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 })
db.sessions.insertOne({ token: "abc", expireAt: new Date(Date.now() + 3600_000) })TTL 索引的两个细节:字段必须是 Date 类型或包含 Date 的数组;过期删除不保证精确到秒(后台任务周期约 60 秒,且负载高时会延后)。
