数据库/协同工具类型
MongoDB 深度指南
MongoDB 的作用定位、何时该用、核心机制(文档/collection/副本集/分片)、常见问题排查(_id、嵌套vs引用、内存)、难点解决方案(事务、分片 shard key、索引、聚合)与生产要点。
- MongoDB
- 数据库
这是 MongoDB 的深度参考文档。上手实操见 Nest 课程第十八章。
一、基础入门(5 分钟上手)
这一节带你把 MongoDB 本身跑起来、用 mongosh 存几条「类 JSON 文档」;Nest 课程那篇讲的是在应用里用 mongoose 连——两篇不重复。
1. 用 Docker 起一个 MongoDB
docker run -d --name mongo -p 27017:27017 mongo:7
- 应用连接地址:
mongodb://localhost:27017。
2. 连上它
docker exec -it mongo mongosh
v6 起官方 shell 叫
mongosh(旧的mongo命令已移除)。看到>提示符就连上了。
3. 最基础的几条命令
// 切到(不存在会自动建)demo 库
> use demo
switched to db demo
// 插一条
> db.users.insertOne({ name: '张三', age: 20, tags: ['vip'] })
{ acknowledged: true, insertedId: ObjectId('64a1f2...') }
// 插多条
> db.users.insertMany([{ name: '李四', age: 25 }, { name: '王五' }])
{ acknowledged: true, insertedCount: 2 }
// 查全部
> db.users.find()
[
{ _id: ObjectId('64a1f2...'), name: '张三', age: 20, tags: [ 'vip' ] },
{ _id: ObjectId('64a1f3...'), name: '李四', age: 25 },
{ _id: ObjectId('64a1f3...'), name: '王五' }
]
// 条件查:age > 22
> db.users.find({ age: { $gt: 22 } })
[ { _id: ObjectId('64a1f3...'), name: '李四', age: 25 } ]
// 改
> db.users.updateOne({ name: '张三' }, { $set: { age: 21 } })
{ acknowledged: true, matchedCount: 1, modifiedCount: 1 }
// 删
> db.users.deleteOne({ name: '王五' })
{ acknowledged: true, deletedCount: 1 }
和 MySQL 最大的不同:文档结构随便加字段(王五没 age 也照存),不用 ALTER TABLE。
4. 数据类型
文档的字段类型(BSON),常用的:
| 类型 | 说明 | 示例 |
|---|---|---|
| String | 字符串,最常用 | "张三" |
| Number | 数字(Int32 / Int64 / Double / Decimal128) | 20、9.9 |
| Boolean | 布尔 | true |
| Date | 日期 | new Date() / ISODate() |
| Null | 空 | null |
| Array | 数组 | ["vip","new"] |
| Object | 嵌套对象 | { city: "北京" } |
| ObjectId | _id 默认类型 | ObjectId(...) |
| Binary | 二进制(少用,大文件存对象存储) | — |
5. 常用语句与操作符
查询用 db.集合.find(条件, 投影),条件用 $ 操作符(掘金《MongoDB 常用增删改查语句》):
db.user.find(); // 全部(= SELECT *)
db.user.find({ age: 22 }); // age = 22
db.user.find({ age: { $gt: 22 } }); // age > 22($gte >=、$lt <、$lte <=)
db.user.find({ age: { $gte: 23, $lte: 26 } }); // 范围
db.user.find({ age: { $in: [20, 25] } }); // 在集合里
db.user.find({ name: /张/ }); // 模糊(含"张",^ 开头 / $ 结尾)
db.user.find({ $or: [{ age: 20 }, { age: 25 }] }); // 或
db.user.find({ age: { $gt: 20 } }, { name: 1, age: 1 }); // 第二参=投影(只取 name、age)
db.user.findOne({ name: "张三" }); // 取一条
db.user.distinct("name"); // 去重某列
db.user.find({ age: { $gt: 20 } }).count(); // 计数
db.user.find().sort({ age: -1 }); // 1 升序 / -1 降序
db.user.find().limit(10).skip(20); // 分页:skip = (页-1) * limit
更新用 db.集合.update(条件, 操作, 选项):
db.user.update({ name: "张三" }, { $set: { age: 21 } }); // 改字段
db.user.update({ name: "张三" }, { $inc: { age: 1 } }); // 自增
db.user.update({ name: "张三" }, { $push: { tags: "vip" } }); // 数组加元素
db.user.update({ sex: "男" }, { $set: { age: 30 } }, { multi: true }); // multi: 改所有匹配
⚠️
update不写$set会整条文档被替换:update({name:"张三"}, {age:16})会把文档变成只有{age:16}!永远带$set。
删除:
db.user.deleteOne({ name: "王五" }); // 删一条
db.user.deleteMany({ age: { $gt: 30 } }); // 删所有匹配
6. 基本使用规则
- 嵌套 vs 引用:会一起读/改就嵌套(文档套对象),会独立变化就引用(存对方
_id)。 _id是 ObjectId 字符串,别用自增数字(分布式下难生成、慢)。- 单文档上限 16MB,大文件/富文本别塞文档,存对象存储,文档里只存 URL。
update永远带$set,否则整条被替换(上面那个坑)。find加条件 + 投影,别无脑find()拉全字段。- 高频查询字段建索引,但别乱建(写时要维护索引)。
7. 核心词汇速记
| 术语 | 一句话 |
|---|---|
| database | 库,demo |
| collection | 集合,对应 MySQL 的表,users |
| document | 文档,一条类 JSON 记录 |
_id | 文档主键,默认 ObjectId 字符串 |
| BSON | MongoDB 存储的二进制 JSON |
| 嵌套 | 文档里可以套对象/数组,不用拆表 JOIN |
二、作用与定位
MongoDB 是文档型数据库,存的是 BSON(类 JSON)文档。核心价值:
- schema-less:同一个 collection 的文档结构可以不同,字段随时加,不用像 MySQL 那样
ALTER TABLE。 - 嵌套存储:一个文档可以层层嵌套对象/数组,不用拆表 JOIN。
- 水平扩展:原生支持分片(sharding),适合海量数据。
一句话:数据结构多变、嵌套深、读写量大、不强依赖跨表事务的场景,用 MongoDB(日志、内容、商品动态属性、用户画像)。
三、何时该用 / 何时用 MySQL
| 场景 | 选择 |
|---|---|
| 结构固定、强事务、关系复杂(订单、账户) | MySQL |
| 结构多变、嵌套深(日志、动态字段、内容) | MongoDB |
| 海量写入、要水平扩展 | MongoDB(分片)或 ClickHouse |
| 需要复杂 JOIN 和多表事务 | MySQL(MongoDB 的 $lookup 弱) |
很多系统两者并存:主业务数据 MySQL,日志/搜索辅助 MongoDB。
四、核心机制速览
- database → collection → document(对应 MySQL 的 库 → 表 → 行,但文档任意结构)。
_id:每个文档的主键,默认 ObjectId(12 字节,含时间戳),字符串。不是自增数字。- 索引:和 MySQL 类似,B 树,建索引加速查询。
_id自动建索引。 - 副本集(Replica Set):一主多从,自动故障转移,高可用。
- 分片(Shard):按 shard key 把数据分散到多台机器,水平扩展。难点在选 shard key。
- 存储引擎 WiredTiger:默认,支持文档级锁、压缩。
副本集一主多从,Primary 宕机后的完整故障转移时序:
一张图看清分片集群的组件与请求流向:
五、常见问题与排查
1. _id 用法
_id 是字符串 ObjectId,不是数字。查询/参数类型写 string。不要用自增 id(分布式下难生成、慢);如果业务要自增序号,用单独的计数器 collection。
2. 嵌套还是引用
- 嵌套:
用户.地址.城市,一次查出,适合「一起读、一起改、一对一或一对很少」。 - 引用:存对方
_id,查时用$lookup(弱 JOIN),适合「一对多且多端会独立改、数据量大」。
原则:会一起用就嵌套,会独立变就引用。别什么都引用(退回到关系型的 JOIN 痛点)。
3. 文档太大
单文档上限 16MB。日志/富文本/附件别塞文档里,存引用(文件存对象存储)。文档太大会拖慢读写和迁移。
4. 查询慢
用 explain() 看是否走了索引(stage 是 IXSCAN 走了,COLLSCAN 全表扫)。给高频查询字段建索引,但别乱建(写时要维护索引)。
5. 连接数
和 MySQL 一样用连接池。mongoose 默认有池。高并发下调 maxPoolSize。
六、难点与解决方案
1. 事务(4.0+)
MongoDB 4.0 起支持多文档事务(副本集),4.2 起支持分片事务。但事务性能不如 MySQL,能用嵌套操作替代就别用事务。mongoose 里:
const session = await Model.startSession();
await session.withTransaction(async () => {
await A.updateOne({...}, { session });
await B.create([{...}], { session });
});
2. 分片与 shard key
分片要选 shard key(数据按它分散)。这是最难的决策:
- 选不好会导致数据倾斜(某个分片特别大)和热点(写入集中到一个分片)。
- 选值基数大、分布均匀、查询常用的字段(如
userId哈希)。 - 避免单调递增的 key(如时间戳,新数据永远落最后一个分片)。
- shard key 选了基本改不了(5.0 起有限支持改),上线前要想清楚。
小项目用副本集就够了,不要一上来就分片。
3. 索引与聚合性能
- 覆盖查询的索引(查询字段 + 返回字段都在索引里)免回表。
- 聚合管道(
$match→$group→$sort)把$match放最前面先过滤、配合索引,否则在大 collection 上极慢。 - 大数据量分析别用 MongoDB 的聚合硬扛,导到 ClickHouse / Elasticsearch。
4. 一致性
默认读偏好 primary(读主,强一致)。读副本可设 secondaryPreferred 分担压力,但有复制延迟(最终一致)。写关注 w:1(主写即返回)vs w:majority(多数副本确认,更安全更慢)——重要数据用 majority。
七、生产环境要点
- 用副本集,别单点(数据安全 + 高可用)。
- 建索引,但监控索引数量(写性能)。
- 控制文档大小(< 1MB 为宜,硬上限 16MB),大字段外置。
- 监控:连接数、慢查询、磁盘、副本集延迟、oplog 窗口。
- 备份:
mongodump逻辑备份 + oplog 增量,或文件级快照。
速查:常见问题对照
| 问题 | 原因 | 解决 |
|---|---|---|
| 查询慢 | 没走索引 | explain()、建索引 |
| 文档写不进去 | 超 16MB | 大字段外置到对象存储 |
| 嵌套查询难取 | 嵌套太深 | 该引用的改引用 |
| 分片数据不均 | shard key 选错 | 选基数大分布均的字段 |
| 弱事务不够 | 多文档要原子 | 用 4.0+ 事务 / 改嵌套单文档 |