MongoDB基础深度解析
面向前端最友好的 NoSQL:用 JSON 一样的文档存数据、用聚合管道做分析、用嵌入还是引用做建模决策,以及与 MySQL 怎么选。
一句话概括
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 还慢。