文章

MongoDB基础深度解析

面向前端最友好的 NoSQL:用 JSON 一样的文档存数据、用聚合管道做分析、用嵌入还是引用做建模决策,以及与 MySQL 怎么选。

MongoDB基础深度解析

一句话概括

MongoDB 是当下最火的文档型 NoSQL,最大特点是直接存 JSON(BSON)。你不用像 MySQL 那样先把对象拆成二维表、再用 JOIN 拼回来——一个博客文章连同作者、标签、评论可以整整齐齐塞进一个文档。对前端来说,后端存的和你 fetch 回来的几乎一模一样,心智负担最小。

但别被”无 Schema”骗了:MongoDB 不是”随便存”,而是”建模决策前移到应用层”。嵌入还是引用、聚合管道怎么写、副本集怎么保证高可用,才是面试真正想听的东西。

核心知识点

1. 文档模型:嵌入(Embed)还是引用(Reference)

关系型数据库遇到”一对多”就建关联表、做 JOIN;MongoDB 的精髓是先想清楚关系形态,再决定嵌进去还是引出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ✅ 嵌入:一对少、一起读写(一篇文章几条评论,查文章时评论也一起要)
{
  _id: ObjectId("..."),
  title: "MongoDB 入门",
  author: { id: ObjectId("..."), username: "alice", avatar: "..." }, // 嵌入子文档
  tags: ["MongoDB", "NoSQL"],                                          // 嵌入数组
  comments: [                                                          // 嵌入评论列表
    { user: "bob", content: "写得好!", createdAt: new Date() }
  ],
  stats: { views: 1523, likes: 89 }
}

// ✅ 引用:一对多、独立读写(一个用户上万个订单,不能全塞进用户文档)
{
  _id: ObjectId("order1"),
  userId: ObjectId("..."),   // 只存对方 _id,查询时再查 users 表
  items: [...],
  total: 299.00
}

一句话决策:数据小、一起用、不常变 → 嵌入;数据大、独立用、会暴涨 → 引用。嵌入读得快(一次查完),但文档会膨胀、数组可能超 16MB 上限;引用灵活,但要额外查一次。

2. CRUD:花括号包裹一切

MongoDB 的查询条件就是 JS 对象,$gt $in $set $inc 这种操作符和前端 filter 神似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const { MongoClient, ObjectId } = require('mongodb');
const client = new MongoClient('mongodb://localhost:27017');
const posts = client.db('blog').collection('posts');

// CREATE
await posts.insertOne({
  title: 'MongoDB 入门', tags: ['MongoDB'], status: 'draft',
  stats: { views: 0 }, createdAt: new Date()
});

// READ:条件 + 嵌套字段 + 排序 + 分页
const hot = await posts.find({
  status: 'published',
  'stats.views': { $gte: 1000 },     // 点号访问嵌套字段
  tags: { $in: ['MongoDB'] }
}).sort({ 'stats.views': -1 }).limit(10).toArray();

// UPDATE:用 $set 改字段、$inc 原子自增,避免先查后改的竞态
await posts.updateOne(
  { _id: new ObjectId("...") },
  { $set: { status: 'published' }, $inc: { 'stats.views': 1 }, $currentDate: { updatedAt: true } }
);

// DELETE
await posts.deleteOne({ _id: new ObjectId("...") });

3. 聚合管道(Aggregation Pipeline):链式的 filter→group→sort

聚合管道是 MongoDB 的”数据分析引擎”。把它想成 JS 数组方法链:每个 stage 接收上一步的输出,处理后交给下一步——$match≈filter、$group≈reduce、$project≈map、$sort≈sort、$limit≈slice。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 统计每位作者的发文数与总阅读量,取阅读量 Top 10
const result = await posts.aggregate([
  { $match: { status: 'published', createdAt: { $gte: new Date('2024-01-01') } } },
  { $group: {
      _id: '$author.username',
      postCount:   { $sum: 1 },
      totalViews:  { $sum: '$stats.views' },
      avgViews:    { $avg: '$stats.views' }
  }},
  { $sort: { totalViews: -1 } },
  { $limit: 10 },
  { $project: { _id: 0, author: '$_id', postCount: 1, totalViews: 1 } }
]).toArray();

MongoDB 也能”连表”,叫 $lookup,但它是聚合里的一个 stage,在内存里做,没有 MySQL 索引驱动的 JOIN 快。频繁 $lookup 通常是建模没做好的信号——能嵌入就别引用再联。

1
2
3
4
5
6
7
8
9
// $lookup 关联作者信息(类似 LEFT JOIN)
await posts.aggregate([
  { $match: { status: 'published' } },
  { $lookup: {
      from: 'users', localField: 'author.id',
      foreignField: '_id', as: 'authorInfo'
  }},
  { $unwind: { path: '$authorInfo', preserveNullAndEmptyArrays: true } }
]).toArray();

4. 高可用与一致性:副本集(Replica Set)

生产里的 MongoDB 从来不是单点,而是副本集:1 个 Primary + 2 个 Secondary。所有写入进 Primary,Primary 把操作记到 oplog,Secondary 异步拉取回放;Primary 挂了,剩下节点在几秒内自动选新主。

1
2
3
4
5
6
// 写入策略 writeConcern:多数节点确认才返回,主挂了也不丢
// w: 'majority' + j: true 是生产推荐
await posts.insertOne(doc, { writeConcern: { w: 'majority', j: true } });

// 读取策略 readPreference:默认读主保证强一致;读从降低主压力
// 'secondaryPreferred' 适合报表类不要求实时的读

ObjectId 也是常考点:12 字节 = 4 字节时间戳 + 3 字节机器 + 2 字节进程 + 3 字节计数器,自带创建时间(ObjectId().getTimestamp()),全局唯一且无需中心协调,所以很多时候不必再单独建 createdAt。

其实你每天都在用

  • 存配置 / 草稿:前端草稿箱、用户偏好设置,直接一个文档存,改字段不用 ALTER TABLE
  • 文章 + 评论 + 标签:一篇博客连同嵌套评论存一个文档,前端拿到就能渲染,不用多次 JOIN
  • 用户行为埋点:每条事件一个文档,结构随业务长,天然适合 Schema-less
  • 聚合做排行榜 / 日报:$group + $sort + $limit 一条管道算出热门文章 Top 10
  • Session / 购物车:购物车这种”一对多、一起读写”的数据,直接嵌入数组最省事
  • 全栈框架的 Model:Next.js / Nest 配 Mongoose,Schema 定义好后 find/save 和写 JS 对象一样
  • 实时变更订阅:Change Streams 监听集合变更,类似前端的事件监听,可做实时通知

常见误解(FAQ)

❌ 误区一:”MongoDB 无 Schema,所以不用设计数据结构”

恰恰相反。MongoDB 把”表结构”的设计成本转移到了文档建模上——嵌入还是引用、数组会不会膨胀、要不要加索引,都得你自己想清楚。Schema-less 是”灵活”不是”随意”,真正的大项目几乎都会用 Mongoose 定义 Schema 做校验。

❌ 误区二:”MongoDB 不用 JOIN,所以比 MySQL 简单、什么都能存”

MongoDB 有 $lookup 能联表,但它在内存里算、不如 MySQL 索引 JOIN 快。如果你的业务本质是大量多对多、复杂报表、强事务(金融订单),MySQL 更合适。MongoDB 擅长的是”嵌套多、结构变、读多写少”的场景。

❌ 误区三:”MongoDB 4.0 才支持事务,所以以前根本不可靠”

4.0 起支持了多文档 ACID 事务,但正确用法是:先用嵌入/引用把 90% 的场景做成单文档原子写(单文档操作天生是事务),只有跨文档强一致才上多文档事务。别因为”有事务了”就回去疯狂 $lookup + 跨表事务,那等于把 MongoDB 用成了慢速 MySQL。

❌ 误区四:”MongoDB 和 MySQL 选一个就行,另一个没用”

这是非此即彼的误区。现实里常见组合:用 MongoDB 存灵活的业务文档(文章、日志、配置),用 MySQL 存强一致的核心账目(订单、流水)。选型看数据形态,不是看哪个”更潮”。

一句话总结

MongoDB 的精髓是“像写 JSON 一样存数据,但建模决策要自己扛”——嵌入还是引用决定了性能上限,聚合管道决定了分析能力,副本集决定了高可用;用对了它比 MySQL 顺手,用错了它比 MySQL 还慢。

本文由作者按照 CC BY 4.0 进行授权