MongoDB 从入门到精通
—— 写给有 MySQL 基础的你
目录
- 先建立心智模型:术语对照表
- 存储引擎的本质区别
- 数据结构的本质区别:BSON 与文档模型
- 安装与连接
- CRUD 操作对照速查
- 查询操作符大全
- 索引
- 聚合框架(替代 JOIN / GROUP BY)
- 数据建模:内嵌 vs 引用
- 事务与一致性
- 高可用与扩展:副本集与分片
- 性能优化与最佳实践
- 常用工具
- 学习路径建议
1. 先建立心智模型:术语对照表
MySQL 的知识不会浪费,几乎每个概念在 MongoDB 里都有对应物,只是实现方式不同。
| MySQL(关系型) | MongoDB(文档型) | 说明 |
|---|---|---|
| Database | Database | 概念一致 |
| Table | Collection(集合) | 一组文档的容器,无固定 schema |
| Row(行) | Document(文档) | 一条 BSON 格式的记录 |
| Column(列) | Field(字段) | 文档内的键值对 |
| Primary Key | _id 字段 |
MongoDB 自动生成 ObjectId 作为主键 |
| Index | Index | 概念一致,用法高度相似 |
| JOIN | $lookup(聚合阶段)或应用层多次查询 |
MongoDB 更推荐"反范式内嵌"来避免 JOIN |
| Schema(强约束) | Schema-less / 动态 Schema | 同一集合里的文档字段可以不同(但不代表不需要设计) |
| GROUP BY | Aggregation Pipeline 的 $group |
表达能力更强,是"流水线"式处理 |
| 事务(InnoDB 默认支持) | 多文档事务(4.0+ 支持,但设计上应尽量避免依赖) | 理念不同:MongoDB 鼓励原子性的单文档操作 |
| 外键约束 | 无原生外键(应用层保证一致性) | 这是最大的思维转变点之一 |
mysql CLI |
mongosh |
官方交互式 Shell,语法是 JavaScript |
关键心态转变:MySQL 设计时先想"如何范式化、避免冗余";MongoDB 设计时先想"这个数据在应用中是如何被读取的,如何组织能一次查询就拿到"。这是贯穿全文最重要的一条原则。
2. 存储引擎的本质区别
2.1 MySQL:InnoDB
- 数据结构:聚簇索引(Clustered Index),基于 B+ 树,主键即数据的物理排序依据;二级索引存储主键值作为指针,回表查询主键索引获取完整行。
- 事务:原生支持 ACID,通过 Undo Log(回滚)+ Redo Log(持久化)+ MVCC 实现隔离级别(默认 REPEATABLE READ)。
- 锁:行级锁 + 间隙锁(Gap Lock),用于防止幻读。
- 存储粒度:以"行"为单位,表结构(schema)在创建表时固定,修改需要
ALTER TABLE(大表可能锁表或走 Online DDL)。
2.2 MongoDB:WiredTiger(自 3.2 起默认引擎)
- 数据结构:同样基于 B 树(不是 B+ 树),但存储的是整份 BSON 文档而非拆分的行列。
- 并发控制:文档级别(document-level)的乐观并发控制,配合 MVCC——这比 MySQL 的行级锁粒度更细,写入吞吐通常更高。
- 压缩:默认对数据启用 snappy 压缩,也可选 zstd/zlib,这是 MySQL InnoDB 默认不做的(需手动开启页压缩)。
- 持久化:Journal(预写日志,类似 Redo Log)+ Checkpoint(约每 60 秒做一次快照落盘),两者共同保证崩溃恢复。
- 历史引擎:早期版本默认是 MMAPv1(内存映射文件,表级锁,性能较差),从 3.2 版本起 WiredTiger 成为默认并集合社区不再使用 MMAPv1,现代 MongoDB(4.2+)已完全移除 MMAPv1。如果你看到很老的教程提到"MongoDB 表锁很严重",说的就是 MMAPv1 时代的旧闻。
- 可插拔架构:MongoDB 的存储引擎是可插拔的(Storage Engine API),WiredTiger 是官方主推的通用引擎,另外还有面向内存计算场景的 In-Memory 引擎(企业版)。
2.3 对比小结
| 维度 | InnoDB | WiredTiger |
|---|---|---|
| 索引结构 | B+ 树,聚簇索引 | B 树,文档存储 |
| 并发粒度 | 行级锁 | 文档级 MVCC |
| 默认压缩 | 无(需手动开) | 有(snappy) |
| Schema 变更成本 | 可能较高(视场景) | 几乎为零(应用层控制) |
| 事务模型 | 原生、深度优化 | 支持但非核心设计理念 |
3. 数据结构的本质区别:BSON 与文档模型
3.1 什么是 BSON
MongoDB 存储的不是 JSON 文本,而是 BSON(Binary JSON)——一种二进制编码格式,在 JSON 基础上增加了更多数据类型,并且便于机器快速解析(不需要像文本 JSON 那样做字符串扫描)。
常见 BSON 类型对比 MySQL 类型:
| MySQL 类型 | BSON / MongoDB 类型 |
|---|---|
| INT / BIGINT | Int32 / Int64 (NumberLong) |
| DECIMAL | Decimal128(精确十进制,避免浮点误差) |
| VARCHAR / TEXT | String(UTF-8) |
| DATETIME | Date(内部存储为 UTC 毫秒时间戳) |
| BOOLEAN | Boolean |
| BLOB | BinData |
| 自增主键 | ObjectId(12 字节,含时间戳+机器标识+计数器,全局唯一,不需要中心化分配) |
| JSON 类型列 | 原生支持嵌套 Object / Array,这是一等公民,不是"额外类型" |
| 无对应 | Array(数组作为字段值,可直接建索引——"多键索引") |
3.2 一个直观例子
MySQL:一个订单需要 3 张表
-- orders 表
CREATE TABLE orders (id INT PRIMARY KEY, user_id INT, created_at DATETIME);
-- order_items 表
CREATE TABLE order_items (id INT PRIMARY KEY, order_id INT, product_name VARCHAR(100), qty INT);
-- 查询一个订单及其明细需要 JOIN
SELECT o.*, i.product_name, i.qty
FROM orders o JOIN order_items i ON o.id = i.order_id
WHERE o.id = 1001;
MongoDB:一个文档就装下整个订单
{
_id: ObjectId("64f1a2..."),
userId: 2001,
createdAt: ISODate("2026-09-01T10:00:00Z"),
items: [
{ productName: "机械键盘", qty: 1, price: 399 },
{ productName: "鼠标垫", qty: 2, price: 29 }
],
status: "shipped"
}
一次 findOne 就能拿到订单和明细,无需 JOIN。这就是文档模型的核心优势:把"经常一起被读取的数据"物理上存在一起。
3.3 动态 Schema 不等于"没有设计"
很多初学者误解"schema-less"为"随便存",这是常见的坑。实践中通常会:
- 用 Schema Validation(
$jsonSchema)在集合层面约束必填字段和类型,相当于"软性的表结构"。 - 用应用层(如 Mongoose、Prisma)定义模型来约束一致性。
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "email"],
properties: {
name: { bsonType: "string" },
email: { bsonType: "string", pattern: "^.+@.+$" },
age: { bsonType: "int", minimum: 0 }
}
}
}
});
4. 安装与连接
4.1 安装(以 Ubuntu 为例,本地开发环境)
# 导入官方 GPG key 并添加源(略,参考 mongodb 官网当前版本命令)
sudo apt-get install -y mongodb-org
sudo systemctl start mongod
sudo systemctl enable mongod
也可以直接使用官方托管服务 MongoDB Atlas(提供免费额度),跳过本地安装,类似于云数据库 RDS 之于 MySQL。
4.2 连接:mongosh
mongosh "mongodb://localhost:27017"
# Atlas 云端示例
mongosh "mongodb+srv://user:password@cluster0.xxxxx.mongodb.net/mydb"
mongosh 本质是一个 Node.js 环境,语法就是 JavaScript,这一点与 mysql CLI(纯 SQL)不同——你可以直接写变量、循环、函数。
use mydb // 相当于 MySQL 的 USE mydb;
show collections // 相当于 SHOW TABLES;
db.users.find().limit(5) // 查询
5. CRUD 操作对照速查
5.1 增(Create)
-- MySQL
INSERT INTO users (name, age) VALUES ('Tom', 25);
// MongoDB
db.users.insertOne({ name: "Tom", age: 25 });
db.users.insertMany([{ name: "A" }, { name: "B" }]); // 批量
5.2 查(Read)
-- MySQL
SELECT name, age FROM users WHERE age > 20 ORDER BY age DESC LIMIT 10;
// MongoDB
db.users.find(
{ age: { $gt: 20 } }, // 查询条件,等价于 WHERE
{ name: 1, age: 1, _id: 0 } // 投影,等价于 SELECT 字段
).sort({ age: -1 }).limit(10);
5.3 改(Update)
-- MySQL
UPDATE users SET age = 26 WHERE name = 'Tom';
// MongoDB
db.users.updateOne(
{ name: "Tom" },
{ $set: { age: 26 } }
);
// 批量更新用 updateMany;$inc 自增、$push 向数组追加元素等
db.users.updateMany({}, { $inc: { loginCount: 1 } });
⚠️ 重要区别:如果忘记加
$set直接写{ age: 26 },MongoDB 会把整个文档替换成{ age: 26 },而不是"更新这一个字段"!这是新手最容易踩的坑之一。
5.4 删(Delete)
-- MySQL
DELETE FROM users WHERE age < 18;
// MongoDB
db.users.deleteMany({ age: { $lt: 18 } });
db.users.deleteOne({ name: "Tom" }); // 只删第一条匹配
6. 查询操作符大全
| SQL | MongoDB 操作符 | 示例 |
|---|---|---|
= |
直接写值 | { age: 25 } |
> / >= |
$gt / $gte |
{ age: { $gt: 20 } } |
< / <= |
$lt / $lte |
{ age: { $lt: 60 } } |
!= |
$ne |
{ status: { $ne: "closed" } } |
IN (...) |
$in |
{ status: { $in: ["a","b"] } } |
NOT IN |
$nin |
{ status: { $nin: ["a"] } } |
LIKE '%abc%' |
$regex |
{ name: { $regex: "abc", $options: "i" } } |
AND |
默认(多字段自动 AND)或 $and |
{ age: { $gt: 20 }, city: "SG" } |
OR |
$or |
{ $or: [{ age: 20 }, { city: "SG" }] } |
IS NULL |
$eq: null 或 $exists: false |
{ deletedAt: null } |
| 数组包含某元素 | $elemMatch / 直接写值 |
{ tags: "vip" }(数组字段自动匹配任一元素) |
7. 索引
索引的思维方式和 MySQL 高度一致:都是为了避免全表扫描(COLLSCAN,相当于 MySQL 的 ALL 全表扫描),本质也是树结构。
// 单字段索引
db.users.createIndex({ age: 1 }); // 1 升序,-1 降序
// 复合索引(顺序很重要,和 MySQL 最左前缀原则完全一致)
db.orders.createIndex({ userId: 1, createdAt: -1 });
// 唯一索引
db.users.createIndex({ email: 1 }, { unique: true });
// 多键索引:数组字段可以直接建索引,MySQL 没有直接对应物
db.articles.createIndex({ tags: 1 });
// TTL 索引:文档到期自动删除,常用于日志/会话数据,MySQL 需要定时任务模拟
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 });
// 文本索引,用于简单全文检索
db.articles.createIndex({ content: "text" });
分析查询计划(对应 MySQL 的 EXPLAIN):
db.users.find({ age: { $gt: 20 } }).explain("executionStats");
// 关注 winningPlan.stage 是 IXSCAN(走索引)还是 COLLSCAN(全表扫描)
8. 聚合框架(替代 JOIN / GROUP BY)
MongoDB 的 Aggregation Pipeline 是数据依次流经一系列"处理阶段"(stage),类似 Unix 管道 |,表达能力比 SQL 的 GROUP BY 更强、更灵活。
8.1 GROUP BY 对照
-- MySQL
SELECT city, COUNT(*) AS cnt, AVG(age) AS avg_age
FROM users
GROUP BY city
HAVING cnt > 5
ORDER BY cnt DESC;
// MongoDB
db.users.aggregate([
{ $group: { _id: "$city", cnt: { $sum: 1 }, avgAge: { $avg: "$age" } } },
{ $match: { cnt: { $gt: 5 } } }, // 相当于 HAVING
{ $sort: { cnt: -1 } }
]);
8.2 JOIN 对照($lookup)
-- MySQL
SELECT o.*, u.name
FROM orders o JOIN users u ON o.user_id = u.id;
// MongoDB
db.orders.aggregate([
{
$lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "userInfo"
}
},
{ $unwind: "$userInfo" } // 把数组展开成单个对象,类似 INNER JOIN 效果
]);
性能提示:
$lookup是可用的逃生舱,但不是首选方案。能用内嵌解决的关联,优先内嵌;真正需要多对多、或关联数据经常独立变化时才用$lookup。
8.3 常用阶段速查
| 阶段 | 作用 | SQL 类比 |
|---|---|---|
$match |
过滤文档 | WHERE |
$project |
选择/计算字段 | SELECT |
$group |
分组聚合 | GROUP BY |
$sort |
排序 | ORDER BY |
$limit / $skip |
分页 | LIMIT / OFFSET |
$lookup |
关联其他集合 | JOIN |
$unwind |
展开数组为多条文档 | 无直接对应,类似"炸开"一对多关系 |
$facet |
一次执行多条并行的子聚合 | 无直接对应,很强大 |
9. 数据建模:内嵌 vs 引用
这是从 MySQL 转到 MongoDB 最重要的设计决策,没有"正确答案",只有"更适合当前读写模式的答案"。
9.1 决策参考表
| 关系类型 | 建议 | 原因 |
|---|---|---|
| 一对少量(1:N,N 较小,如"文章-评论(<100条)") | 内嵌 | 一次查询拿全部数据,避免 JOIN |
| 一对海量(1:N,N 可能无限增长,如"用户-日志") | 引用(外部集合存主表 _id) |
内嵌会导致单文档无限膨胀,超过 16MB 文档大小上限 |
| 多对多 | 引用 + 中间集合,或双向引用数组 | 类似 MySQL 的关联表,但更灵活 |
| 数据是否经常单独更新 | 若关联数据独立变化频繁,倾向引用 | 避免内嵌数据频繁重复更新导致写放大 |
| 数据是否需要跨文档强一致读取 | 需要则引用 + 事务,否则内嵌更简单 | 内嵌天然保证同一文档内的原子性 |
9.2 常见 Schema 设计模式(选读,进阶)
- Subset Pattern(子集模式):只在主文档内嵌"最常访问的部分"数据(如商品的前 10 条评论),其余存在独立集合,兼顾读性能和文档体积。
- Bucket Pattern(桶模式):如 IoT 时序数据,把同一小时的多条读数打包进一个"桶"文档,减少文档数量、提高写入和聚合效率。
- Extended Reference Pattern:引用其他集合时,把常用的几个字段"顺便冗余一份"过来(如订单里冗余存一份用户名),减少
$lookup次数,代价是需要处理冗余字段的更新同步。
16MB 限制提醒:单个 BSON 文档上限是 16MB,这是 MySQL 没有的硬约束,设计无限增长的内嵌数组(如"给用户加好友")时要格外注意,通常改为引用或分桶。
10. 事务与一致性
- 单文档操作天然原子:MongoDB 对单个文档的任何更新(哪怕更新多个字段/数组元素)都是原子的,这也是为什么好的 Schema 设计会尽量把"需要一起变化的数据"放进同一文档——从而根本不需要事务。
- 多文档事务:自 4.0(副本集)/ 4.2(分片集群)起原生支持类似 MySQL 的多语句事务:
const session = db.getMongo().startSession();
session.startTransaction();
try {
session.getDatabase("mydb").accounts.updateOne({ _id: 1 }, { $inc: { balance: -100 } });
session.getDatabase("mydb").accounts.updateOne({ _id: 2 }, { $inc: { balance: 100 } });
session.commitTransaction();
} catch (e) {
session.abortTransaction();
}
- Read/Write Concern:这是 MongoDB 特有、MySQL 没有直接对应的一套可调一致性/持久性级别机制(因为 MongoDB 天生是分布式的):
writeConcern: { w: "majority" }—— 写入需要被大多数副本节点确认才算成功,类似"半同步复制"的加强版。readConcern: "majority"—— 只读取已被多数节点确认的数据,避免读到可能被回滚的数据。- 这一套机制在 MySQL 主从复制里没有细粒度的直接对照,可以类比为"你自己可以调节 CAP 中一致性和可用性的取舍旋钮"。
实践建议:不要把 MongoDB 当成"可以到处用事务的关系型数据库",事务是补救手段,不是设计起点;先想清楚建模,事务需求会大幅减少。
11. 高可用与扩展:副本集与分片
11.1 副本集(Replica Set)对比主从复制
| MySQL 主从复制 | MongoDB 副本集 |
|---|---|
| 一主多从,手动/半自动切换(需 MHA/Orchestrator 等工具) | 一主(Primary)多从(Secondary),原生自动故障转移(Failover),无需额外组件 |
| 从库默认异步复制 | 默认也是异步,但可通过 Write Concern 调整为近似同步 |
| 读写分离需应用层/中间件路由 | 驱动原生支持 readPreference(如 secondaryPreferred) |
11.2 分片(Sharding)对比分库分表
| MySQL 分库分表 | MongoDB Sharding |
|---|---|
| 通常需要 ProxySQL/Vitess/中间件,业务侵入性较大 | 原生内置分片架构,对应用基本透明 |
| 分片键需手工设计路由逻辑 | 需选择 Shard Key,之后 mongos 路由层自动处理请求分发 |
| 扩容需要复杂的数据迁移方案 | 支持 chunk 自动在分片间迁移均衡(Balancer) |
Shard Key 的选择原则和 MySQL 分库键设计思路一致:需要高基数、写入均匀分布、尽量匹配常用查询条件,选不好一样会出现热点分片问题。
12. 性能优化与最佳实践
- 像 MySQL 一样重视索引:没有命中索引的查询在数据量大时同样会拖垮 MongoDB,
explain()要养成习惯用。 - 投影只取需要的字段:
find(query, { field: 1 }),减少网络和内存开销,等价于不要SELECT *。 - 警惕无界数组增长:数组字段和索引都要考虑上限,16MB 文档限制、以及索引对超大数组的性能影响。
- 合理使用
$lookup,但不要滥用:多次$lookup相当于多次 JOIN,仍然有性能代价,能内嵌就内嵌。 - 连接池:MongoDB 驱动同样有连接池概念,生产环境要像调 MySQL 连接池一样调
maxPoolSize。 - 监控工具:
mongostat(类似mysqladmin status)、mongotop(看哪个集合读写最热)、Atlas 自带的 Performance Advisor 会自动建议缺失索引。 - 避免"关系型思维"陷阱:不要把每一个 MySQL 表原封不动建成一个 Collection 再疯狂
$lookup,那样等于用 MongoDB 的成本换 MySQL 的体验,还更差。
13. 常用工具
| 用途 | MySQL 生态 | MongoDB 生态 |
|---|---|---|
| 图形化管理 | Workbench / Navicat | MongoDB Compass(官方免费GUI) |
| 云托管服务 | RDS / Aurora | MongoDB Atlas |
| ORM/ODM | MyBatis / Sequelize | Mongoose(Node.js)/ 各语言官方驱动 |
| 命令行 | mysql |
mongosh |
| 备份 | mysqldump |
mongodump / mongorestore |
| 导入导出 | LOAD DATA / CSV |
mongoimport / mongoexport |
14. 学习路径建议
第 1 周(入门)
- 装好 MongoDB / 注册 Atlas 免费集群,跑通 mongosh 连接。
- 熟练 CRUD + 常用查询操作符(对照第 5、6 章反复练习)。
第 2 周(进阶)
- 掌握索引设计与 explain() 分析(第 7 章)。
- 吃透聚合框架,能用 $group / $lookup 替代你原来写的复杂 SQL(第 8 章)。
第 3 周(建模思维转变,最关键) - 挑一个你熟悉的 MySQL 项目,尝试用"内嵌 vs 引用"重新设计一遍 Schema(第 9 章),这是拉开新手与熟练者差距的核心能力。 - 理解 16MB 限制、Bucket/Subset 等设计模式适用场景。
第 4 周(生产化)
- 学习事务的正确使用边界(第 10 章)。
- 了解副本集和分片的原理,哪怕暂时用不到,也要知道它解决什么问题(第 11 章)。
- 过一遍性能优化清单(第 12 章),养成用 Compass 或 mongostat 观察系统状态的习惯。
之后(持续精进) - 官方文档(MongoDB Manual)是最权威、更新最及时的资料,配合实际项目反复实践 Schema 设计决策,才是从"会用"到"精通"的关键路径——这一点和数据库学习的规律其实和 MySQL 是一样的。
文档说明:本文基于 MongoDB 通用概念与 WiredTiger 存储引擎(现行默认引擎)编写,具体版本细节(如某些语法糖、Atlas 界面)可能随版本更新略有变化,建议关键操作以你所用版本的官方 Manual 为准。