mongoDB
视频:
理论篇
一:MongoDB 是什么?为什么需要它?
1.1 从一个真实问题说起
想象你在运营一款游戏,有十多亿用户数据需要存储。如果用 MySQL:
-- MySQL 的痛点:表结构是固定的
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100),
level INT,
-- 未来可能要加的字段?只能预留...
reserved_field_1 VARCHAR(255),
reserved_field_2 VARCHAR(255),
reserved_field_3 VARCHAR(255) -- 浪费空间!
);
-- 某天产品说:"给VIP用户加个专属徽章字段"
-- → 需要 ALTER TABLE,十亿行数据的表改结构?噩梦!
MongoDB 的回答:不需要固定表结构。
// 普通用户
{ "_id": 1, "name": "张三", "level": 10 }
// VIP用户 —— 直接多加字段,无需改结构
{ "_id": 2, "name": "李四", "level": 50, "vip": true, "badge": "黄金徽章" }
// 两条数据可以有不同的字段,和平共处在同一个集合中
1.2 核心概念映射
| MySQL 概念 | MongoDB 概念 | 说明 |
|---|---|---|
| Database | Database | 完全一致 |
| Table(表) | Collection(集合) | 集合不需要预定义结构 |
| Row(行) | Document(文档) | 文档是类 JSON 格式 |
| Column(列) | Field(字段) | 文档间字段可以不一致 |
| PRIMARY KEY | _id |
每个文档自动拥有 _id 主键 |
1.3 文档(Document)—— MongoDB 的灵魂
文档是 MongoDB 的基本数据单元,长得像 JSON:
{
"_id": ObjectId("507f1f77bcf86cd799439011"), // 自动生成的主键
"name": "王五",
"age": 28,
"address": { // 可以嵌套对象
"city": "北京",
"district": "海淀区"
},
"hobbies": ["编程", "游泳"], // 可以包含数组
"created_at": ISODate("2026-03-18T10:00:00Z") // 支持日期类型
}
关键特点:
- 每个文档必须有
_id字段(不指定则自动生成 ObjectId) - 字段值支持丰富的数据类型:字符串、数字、布尔、数组、嵌套文档、日期、二进制等
- 同一集合中的文档不要求拥有相同的字段
1.4 查询语法对比
| 操作 | MySQL | MongoDB |
|---|---|---|
| 查询 | SELECT * FROM users WHERE age > 25 |
db.users.find({ age: { $gt: 25 } }) |
| 插入 | INSERT INTO users (name, age) VALUES ('张三', 25) |
db.users.insertOne({ name: "张三", age: 25 }) |
| 更新 | UPDATE users SET age = 26 WHERE name = '张三' |
db.users.updateOne({ name: "张三" }, { $set: { age: 26 } }) |
| 删除 | DELETE FROM users WHERE age < 18 |
db.users.deleteMany({ age: { $lt: 18 } }) |
二:存储引擎 —— WiredTiger 深度解析
存储引擎是数据库的"心脏",决定了数据如何在磁盘上存储和读取。MongoDB 默认使用 WiredTiger。
2.1 BSON 格式
MongoDB 在内部并不直接存储 JSON,而是使用 BSON(Binary JSON):
BSON 相比 JSON 的优势:
| 特性 | JSON | BSON |
|---|---|---|
| 格式 | 纯文本 | 二进制 |
| 解析速度 | 需要解析文本 | 直接内存映射,更快 |
| 数据类型 | 有限(字符串、数字等) | 丰富(增加 Date、Binary、ObjectId 等) |
| 空间 | 较紧凑 | 略大(包含长度前缀便于遍历) |
2.2 数据页(Page)
WiredTiger 不会一条一条地读写文档,而是将数据组织成 32KB 的数据页(Page)
为什么要用数据页?
- 磁盘 I/O 的最小单位是"块",批量读写比逐条读写高效得多
- 一次读入一页,连带周围文档一起加载到内存,利用空间局部性
2.3 索引结构 —— 变种 B+ 树
MongoDB 的 B+ 树有一个重要变种——写时复制(Copy On Write):
Copy On Write 的精妙之处:
- 读操作:继续读旧版本的节点,不被阻塞
- 写操作:复制要修改的节点,在副本上修改
- 结果:读写互不阻塞,高并发性能优秀
2.4 缓存(Cache)
- WiredTiger 维护内存缓存,存放热点数据页
- 缓存命中 → 纳秒级响应;缓存未命中 → 毫秒级磁盘 I/O
- 默认使用
(RAM - 1GB) / 2或256MB(取较大值)作为缓存大小
2.5 数据持久化机制
这是 WiredTiger 最精密的部分,通过 Journal + Checkpoint 双重保障数据不丢失:
两层保障的含义:
| 机制 | 写入方式 | 触发频率 | 作用 |
|---|---|---|---|
| Journal | 顺序写入(append-only) | 每次写操作 | 崩溃恢复的"保险",记录每个操作 |
| Checkpoint | 随机写入(批量刷脏页) | 默认每 60 秒 | 真正把数据落盘 |
崩溃恢复场景:
假设在两次 Checkpoint 之间服务器宕机了 → 重启后,从最后一个 Checkpoint 开始,重放 Journal 日志中的操作 → 数据恢复完成,不丢失
三:Server 层架构
Server 层是连接"用户查询"和"存储引擎"的桥梁:
各模块职责
| 模块 | 职责 | 类比 |
|---|---|---|
| 连接管理模块 | 管理客户端 TCP 连接、认证 | 餐厅前台 |
| 查询解析器 | 解析查询语法,检查语法错误 | 服务员确认订单 |
| 查询优化器 | 选择最优索引,生成执行计划 | 厨师长决定烹饪方案 |
| 执行器 | 按计划调用存储引擎接口 | 厨师执行烹饪 |
查询语句到存储引擎的映射:
db.users.find({ age: 25 }) → WiredTiger.search()
db.users.updateOne(...) → WiredTiger.update()
db.users.insertOne(...) → WiredTiger.insert()
db.users.deleteOne(...) → WiredTiger.remove()
四:从单机到分布式集群
4.1 单机 MongoDB 的瓶颈
单机的三大瓶颈:
- CPU:查询并发量大时处理不过来
- 内存:热数据超出内存容量,缓存命中率下降
- 磁盘:数据量过大,I/O 成为瓶颈
解决方案 → 分布式集群
4.2 分布式集群完整架构
4.3 三大核心组件
① 分片(Shard)—— 水平拆分数据
- 将数据按分片键(如
_id)的范围拆分到不同节点 - 每个分片只存储一部分数据,分摊存储和计算压力
- 支持范围分片和哈希分片两种策略
② mongos 路由 —— 请求的"交通警察"
- 客户端不直接连接分片,而是连接 mongos
- mongos 根据分片信息精确路由请求到对应分片
- 如果查询涉及多个分片,mongos 负责合并结果
③ 副本集(Replica Set)—— 高可用保障
- 每个分片都是一个副本集(通常 1 主 + 2 副本)
- 主节点:处理所有写操作,也可处理读
- 副本节点:通过 oplog 同步数据,可分担读操作
- 自动故障转移:主节点宕机后,副本节点自动选举出新主节点
4.4 完整的读写流程
读流程:
写流程:
副本节点Journal日志WiredTiger分片主节点mongos客户端副本节点Journal日志WiredTiger分片主节点mongos客户
五:理论篇总结 —— 知识全景图
实践篇
一:环境搭建
1.1 安装 MongoDB(以 Docker 方式为例,最简单)
# 拉取 MongoDB 官方镜像
docker pull mongo:7.0
# 启动 MongoDB 容器
docker run -d \
--name mongodb \
-p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=password123 \
-v mongodb_data:/data/db \
mongo:7.0
# 验证是否启动成功
docker ps | grep mongodb
1.2 连接 MongoDB
# 方式1:使用 mongosh(官方 Shell)
docker exec -it mongodb mongosh -u admin -p password123
# 方式2:使用连接字符串(应用程序中常用)
# mongodb://admin:password123@localhost:27017
进入 mongosh 后,你会看到:
Current Mongosh Log ID: 65f...
Connecting to: mongodb://127.0.0.1:27017
Using MongoDB: 7.0.x
Using Mongosh: 2.x.x
test> ← 这里可以输入命令了
二:CRUD 基础操作
2.1 数据库和集合操作
// 查看所有数据库
show dbs
// 切换/创建数据库(使用即创建)
use gamedb
// 查看当前数据库
db
// 查看当前数据库的所有集合
show collections
// 创建集合(通常不需要显式创建,插入文档时自动创建)
db.createCollection("players")
// 删除集合
db.players.drop()
// 删除数据库
db.dropDatabase()
2.2 插入文档(Create)
// 插入单个文档
db.players.insertOne({
name: "张三",
level: 25,
server: "东方明珠",
class: "战士",
equipment: ["屠龙刀", "黄金甲"],
stats: {
hp: 5000,
mp: 1000,
attack: 350
},
created_at: new Date()
})
// 插入多个文档
db.players.insertMany([
{
name: "李四",
level: 30,
server: "东方明珠",
class: "法师",
equipment: ["寒冰法杖"],
stats: { hp: 3000, mp: 5000, attack: 500 },
vip: true, // 注意:只有这个文档有 vip 字段!
created_at: new Date()
},
{
name: "王五",
level: 15,
server: "星辰大海",
class: "刺客",
equipment: ["影刃", "暗影斗篷"],
stats: { hp: 3500, mp: 2000, attack: 450 },
created_at: new Date()
},
{
name: "赵六",
level: 42,
server: "东方明珠",
class: "战士",
equipment: ["天罚之剑", "不灭战甲", "龙鳞盾"],
stats: { hp: 8000, mp: 1500, attack: 600 },
guild: "王者之巅", // 只有这个文档有 guild 字段!
created_at: new Date()
}
])
2.3 查询文档(Read)
// ===== 基础查询 =====
// 查询所有文档
db.players.find()
// 美化输出
db.players.find().pretty()
// 查询特定条件
db.players.find({ class: "战士" })
// 查询单个文档
db.players.findOne({ name: "张三" })
// ===== 比较运算符 =====
// $gt 大于 | $gte 大于等于 | $lt 小于 | $lte 小于等于 | $ne 不等于
db.players.find({ level: { $gt: 20 } }) // 等级大于20
db.players.find({ level: { $gte: 15, $lte: 30 }}) // 等级在15到30之间
// ===== 逻辑运算符 =====
// $and(默认多条件就是 AND)
db.players.find({ class: "战士", level: { $gt: 20 } })
// $or
db.players.find({
$or: [
{ class: "法师" },
{ level: { $gt: 40 } }
]
})
// ===== 数组查询 =====
// 包含某个元素
db.players.find({ equipment: "屠龙刀" })
// 数组长度
db.players.find({ equipment: { $size: 3 } }) // 装备数量为3的玩家
// ===== 嵌套文档查询 =====
// 使用点号访问嵌套字段
db.players.find({ "stats.attack": { $gt: 400 } })
// ===== 字段存在性查询 =====
// 查找有 vip 字段的文档
db.players.find({ vip: { $exists: true } })
// 查找有公会的玩家
db.players.find({ guild: { $exists: true } })
// ===== 投影(选择返回的字段)=====
// 只返回 name 和 level(1 表示包含,0 表示排除)
db.players.find({}, { name: 1, level: 1, _id: 0 })
// ===== 排序、跳过、限制 =====
// 按等级降序排列
db.players.find().sort({ level: -1 })
// 分页:跳过前2条,取2条(第2页,每页2条)
db.players.find().sort({ level: -1 }).skip(2).limit(2)
// ===== 计数 =====
db.players.countDocuments({ server: "东方明珠" })
2.4 更新文档(Update)
// ===== 更新单个文档 =====
// $set:设置字段值(字段不存在则创建)
db.players.updateOne(
{ name: "张三" }, // 过滤条件
{ $set: { level: 26, title: "初级勇者" } } // 更新操作
)
// $inc:数值增减
db.players.updateOne(
{ name: "张三" },
{ $inc: { level: 5 } } // 等级 +5
)
// $unset:删除字段
db.players.updateOne(
{ name: "李四" },
{ $unset: { vip: "" } } // 移除 vip 字段
)
// ===== 数组操作 =====
// $push:向数组添加元素
db.players.updateOne(
{ name: "张三" },
{ $push: { equipment: "凤凰之翼" } }
)
// $pull:从数组移除元素
db.players.updateOne(
{ name: "张三" },
{ $pull: { equipment: "黄金甲" } }
)
// $addToSet:向数组添加元素(不重复)
db.players.updateOne(
{ name: "张三" },
{ $addToSet: { equipment: "屠龙刀" } } // 已存在则不添加
)
// ===== 更新多个文档 =====
// 所有战士的血量 +1000
db.players.updateMany(
{ class: "战士" },
{ $inc: { "stats.hp": 1000 } }
)
// ===== upsert:不存在则插入 =====
db.players.updateOne(
{ name: "孙七" },
{ $set: { name: "孙七", level: 1, class: "弓箭手" } },
{ upsert: true } // 如果孙七不存在,就创建这个文档
)
2.5 删除文档(Delete)
// 删除单个文档
db.players.deleteOne({ name: "孙七" })
// 删除多个文档
db.players.deleteMany({ level: { $lt: 10 } }) // 删除等级小于10的
// 删除所有文档(保留集合)
db.players.deleteMany({})
三:索引 —— 性能的关键
3.1 为什么需要索引?
// 没有索引时:全集合扫描(Collection Scan)
// 假设有 1000 万玩家,查找 name = "张三"
// → 需要逐条检查所有文档 😱
// 有索引时:B+ 树查找
// → 几次树节点跳转就找到了 ⚡
3.2 索引操作
// ===== 创建索引 =====
// 单字段索引(1: 升序,-1: 降序)
db.players.createIndex({ name: 1 })
// 复合索引
db.players.createIndex({ server: 1, level: -1 })
// 唯一索引
db.players.createIndex({ name: 1 }, { unique: true })
// TTL 索引(文档过期自动删除,适合日志、会话等)
db.sessions.createIndex(
{ created_at: 1 },
{ expireAfterSeconds: 3600 } // 1小时后自动删除
)
// 文本索引(全文搜索)
db.articles.createIndex({ content: "text" })
// ===== 查看索引 =====
db.players.getIndexes()
// ===== 删除索引 =====
db.players.dropIndex({ name: 1 })
// 删除所有非 _id 索引
db.players.dropIndexes()
3.3 执行计划分析(explain)
// 查看查询的执行计划
db.players.find({ name: "张三" }).explain("executionStats")
// 关键字段解读:
// "stage": "COLLSCAN" → 全集合扫描(没用到索引,慢!)
// "stage": "IXSCAN" → 使用了索引扫描(快!)
// "totalDocsExamined" → 扫描了多少文档
// "executionTimeMillis" → 执行耗时(毫秒)
实际示例:
// 创建索引前
db.players.find({ level: { $gt: 20 } }).explain("executionStats")
// → stage: "COLLSCAN", totalDocsExamined: 4(全表扫描)
// 创建索引
db.players.createIndex({ level: 1 })
// 创建索引后
db.players.find({ level: { $gt: 20 } }).explain("executionStats")
// → stage: "IXSCAN", totalDocsExamined: 3(只扫描匹配的文档)
四:聚合管道
聚合管道是 MongoDB 最强大的数据处理功能,类似 SQL 的 GROUP BY + 各种函数:
4.1 常用聚合操作
// ===== 示例1:统计每个职业的玩家数量和平均等级 =====
db.players.aggregate([
// 阶段1:按职业分组,统计数量和平均等级
{
$group: {
_id: "$class", // 按 class 字段分组
count: { $sum: 1 }, // 计数
avgLevel: { $avg: "$level" }, // 平均等级
maxLevel: { $max: "$level" } // 最高等级
}
},
// 阶段2:按数量降序排列
{ $sort: { count: -1 } }
])
// 输出类似:
// { _id: "战士", count: 2, avgLevel: 33.5, maxLevel: 42 }
// { _id: "法师", count: 1, avgLevel: 30, maxLevel: 30 }
// { _id: "刺客", count: 1, avgLevel: 15, maxLevel: 15 }
// ===== 示例2:筛选 → 分组 → 投影 =====
db.players.aggregate([
// 阶段1:只看"东方明珠"服务器
{ $match: { server: "东方明珠" } },
// 阶段2:按职业分组
{
$group: {
_id: "$class",
players: { $push: "$name" }, // 收集玩家名字到数组
totalAttack: { $sum: "$stats.attack" } // 总攻击力
}
},
// 阶段3:美化输出
{
$project: {
_id: 0,
class: "$_id",
players: 1,
totalAttack: 1
}
}
])
// ===== 示例3:$unwind 展开数组 =====
// 统计哪些装备最受欢迎
db.players.aggregate([
{ $unwind: "$equipment" }, // 将数组拆成多条文档
{
$group: {
_id: "$equipment",
count: { $sum: 1 }
}
},
{ $sort: { count: -1 } },
{ $limit: 5 } // 取前5
])
// ===== 示例4:$lookup 关联查询(类似 SQL JOIN)=====
// 假设有一个 guilds 集合
db.guilds.insertMany([
{ name: "王者之巅", leader: "赵六", memberCount: 50 },
{ name: "星辰战队", leader: "某人", memberCount: 30 }
])
db.players.aggregate([
{
$lookup: {
from: "guilds", // 关联的集合
localField: "guild", // 本集合的字段
foreignField: "name", // 目标集合的字段
as: "guildInfo" // 输出字段名
}
},
{ $match: { guild: { $exists: true } } }
])
五:pymongo —— 在 Python 里操作 MongoDB
mongosh 里的语法到了 Python 里就是 pymongo(pip install pymongo),套路一一对应,只是包了一层方法调用。
连接与库 / 集合的选择:
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
# 库和集合都不用提前建:不存在时第一次写入会自动创建(惰性创建)
db = client["game"] # 等价于 mongosh 的 use game
players = db["players"] # 等价于 db.players
CRUD 对照:
# 插入:insertOne → insert_one
result = players.insert_one({"name": "张三", "class": "战士", "level": 42})
print(result.inserted_id) # 自动生成的 ObjectId
players.insert_many([ # insertMany → insert_many
{"name": "李四", "class": "法师", "level": 30},
{"name": "王五", "class": "刺客", "level": 15},
])
# 查询:find() 返回游标,直接 for 遍历;find_one 返回第一条文档
for p in players.find({"level": {"$gte": 30}}, {"name": 1, "level": 1, "_id": 0}):
print(p) # 投影 {字段: 1/0} 和 mongosh 完全一样 "张三"})
# 操作符原样搬过来:$gt / $in / $or 一个都不变,链上 sort / limit / skip 即可
rows = list(players.find({"level": {"$gt": 20}}).sort("level", -1).limit(10).skip(5))
count = players.count_documents({"class": "战士"})
# 更新:updateOne → update_one,$set / $inc 照旧
res = players.update_one({"name": "张三"}, {"$set": {"level": 43}})
print(res.modified_count) # 实际改了几条,一眼可见
players.update_many({"class": "战士"}, {"$inc": {"level": 1}})
# upsert:查不到就插入,加 upsert=True
players.update_one({"name": "赵六"}, {"$set": {"level": 1}}, upsert=True)
# 删除
players.delete_one({"name": "赵六"})
players.delete_many({"level": {"$lt": 10}})
聚合管道:把 mongosh 的数组原样传给 aggregate:
pipeline = [
{"$match": {"server": "东方明珠"}},
{"$group": {"_id": "$class", "count": {"$sum": 1}, "avgLevel": {"$avg": "$level"}}},
{"$sort": {"count": -1}},
]
for row in players.aggregate(pipeline):
print(row)
三个我踩过的坑:
_id存进去是 ObjectId 类型,查的时候传字符串"665f..."是查不到的,要先ObjectId("665f...")包一层(from bson import ObjectId)。- Python 的
datetime写进 MongoDB 会存成 UTC:本地时间直接存,读出来差 8 小时。要么存之前自己转 UTC,要么读出来加时区换算。 find()是游标,只能遍历一次,要反复用就list(...)落成列表;但数据量大时全 list 会把结果堆进内存,注意用skip/limit分页,或基于_id做范围查询翻页。
六:事务、备份与运维速记
多文档事务(4.0+,需副本集/分片集群,单机版不支持):
with client.start_session() as s:
with s.start_transaction():
accounts.update_one({"name": "A"}, {"$inc": {"balance": -100}}, session=s)
accounts.update_one({"name": "B"}, {"$inc": {"balance": 100}}, session=s)
# with 正常退出自动提交,任一步抛异常自动回滚
💡 MongoDB 的设计哲学是"能用单文档原子性就别开事务"——把需要一起改的数据内嵌成一个文档(03-理解篇的反范式化思路正是为此服务),事务是兜底手段不是常态。这也是它和关系型"先拆表再 JOIN"相反的根源。
备份与恢复:
mongodump --uri="mongodb://localhost:27017" --db=game -o ./backup # 导出 BSON 目录
mongorestore --uri="mongodb://localhost:27017" --drop ./backup/game # 恢复;--drop 先清同名集合
慢查询排查三件套:
db.players.find({"level": {"$gt": 30}}).explain("executionStats")
// 重点看:totalDocsExamined 扫了多少 vs nReturned 返回几条——比值越接近 1 索引越高效
db.players.find({"level": {"$gt": 30}}).hint({"level_1"}) // 强制走指定索引做 A/B 对比
db.setProfilingLevel(1, { slowms: 100 }) // 记录 100ms 以上的慢查询
db.system.profile.find().sort({ ts: -1 }).limit(5) // 看最近的慢查询日志
Change Streams 一句话:collection.watch() 监听集合的变更事件流(insert/update/delete),适合做缓存失效通知、数据同步——知道有这个东西,真用到再深挖。
速查表
┌──────────────────────────────────────────────────────────────┐
│ MongoDB 操作速查表 │
├──────────────┬───────────────────────────────────────────────┤
│ 插入 │ insertOne({}) / insertMany([{}, {}]) │
│ 查询 │ find({条件}, {投影}) / findOne({}) │
│ 更新 │ updateOne({条件}, {$set:{}}) │
│ │ updateMany({条件}, {$inc:{}}) │
│ 删除 │ deleteOne({}) / deleteMany({}) │
├──────────────┼───────────────────────────────────────────────┤
│ 比较 │ $gt $gte $lt $lte $ne $in $nin │
│ 逻辑 │ $and $or $not $nor │
│ 数组 │ $push $pull $addToSet $size $elemMatch │
│ 字段 │ $set $unset $inc $rename $exists │
├──────────────┼───────────────────────────────────────────────┤
│ 聚合阶段 │ $match → $group → $sort → $project → $limit │
│ │ $unwind $lookup $count $skip │
├──────────────┼───────────────────────────────────────────────┤
│ 索引 │ createIndex({field:1}) / getIndexes() │
│ │ dropIndex() / explain("executionStats") │
└──────────────┴───────────────────────────────────────────────┘
⬅️ 01-Redis 学习路线 🏠 00-数据库 ➡️ 03-MongoDB 理解
💬 评论