mongoDB

视频:

🎬 B站视频 BV1qyebzyEAj

理论篇

一: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 核心概念映射

diagram-1773802384732-4a5de46b
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)

image-f278dfb2

BSON 相比 JSON 的优势:

特性 JSON BSON
格式 纯文本 二进制
解析速度 需要解析文本 直接内存映射,更快
数据类型 有限(字符串、数字等) 丰富(增加 Date、Binary、ObjectId 等)
空间 较紧凑 略大(包含长度前缀便于遍历)

2.2 数据页(Page)

WiredTiger 不会一条一条地读写文档,而是将数据组织成 32KB 的数据页(Page)diagram-1773802696725-534ec3e3

为什么要用数据页?

  • 磁盘 I/O 的最小单位是"块",批量读写比逐条读写高效得多
  • 一次读入一页,连带周围文档一起加载到内存,利用空间局部性

2.3 索引结构 —— 变种 B+ 树

image-2ca6a93f

MongoDB 的 B+ 树有一个重要变种——写时复制(Copy On Write)

Copy On Write 的精妙之处:

  • 读操作:继续读旧版本的节点,不被阻塞
  • 写操作:复制要修改的节点,在副本上修改
  • 结果:读写互不阻塞,高并发性能优秀

2.4 缓存(Cache)

image-1be49a59
  • WiredTiger 维护内存缓存,存放热点数据页
  • 缓存命中 → 纳秒级响应;缓存未命中 → 毫秒级磁盘 I/O
  • 默认使用 (RAM - 1GB) / 2256MB(取较大值)作为缓存大小

2.5 数据持久化机制

这是 WiredTiger 最精密的部分,通过 Journal + Checkpoint 双重保障数据不丢失:

image-0ee6d94b

两层保障的含义:

机制 写入方式 触发频率 作用
Journal 顺序写入(append-only) 每次写操作 崩溃恢复的"保险",记录每个操作
Checkpoint 随机写入(批量刷脏页) 默认每 60 秒 真正把数据落盘

崩溃恢复场景:

假设在两次 Checkpoint 之间服务器宕机了 → 重启后,从最后一个 Checkpoint 开始,重放 Journal 日志中的操作 → 数据恢复完成,不丢失

三:Server 层架构

Server 层是连接"用户查询"和"存储引擎"的桥梁:

image-a1f3e0eb

各模块职责

模块 职责 类比
连接管理模块 管理客户端 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 的瓶颈

image-e3ccafcf

单机的三大瓶颈:

  • CPU:查询并发量大时处理不过来
  • 内存:热数据超出内存容量,缓存命中率下降
  • 磁盘:数据量过大,I/O 成为瓶颈

解决方案 → 分布式集群

4.2 分布式集群完整架构

diagram-1773804390358-dcb7a505

4.3 三大核心组件

① 分片(Shard)—— 水平拆分数据

diagram-1773804597019-6afda0fd
  • 将数据按分片键(如 _id)的范围拆分到不同节点
  • 每个分片只存储一部分数据,分摊存储和计算压力
  • 支持范围分片哈希分片两种策略

② mongos 路由 —— 请求的"交通警察"

image-8a20f7c6
  • 客户端不直接连接分片,而是连接 mongos
  • mongos 根据分片信息精确路由请求到对应分片
  • 如果查询涉及多个分片,mongos 负责合并结果

③ 副本集(Replica Set)—— 高可用保障

diagram-1773805214486-805826f2 image-67cf3499
  • 每个分片都是一个副本集(通常 1 主 + 2 副本)
  • 主节点:处理所有写操作,也可处理读
  • 副本节点:通过 oplog 同步数据,可分担读操作
  • 自动故障转移:主节点宕机后,副本节点自动选举出新主节点

4.4 完整的读写流程

读流程:

image-f1cc08cd

写流程:

副本节点Journal日志WiredTiger分片主节点mongos客户端副本节点Journal日志WiredTiger分片主节点mongos客户image-79b39975

五:理论篇总结 —— 知识全景图

image-8a57b98d

实践篇

一:环境搭建

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 + 各种函数:

image-e1397f48

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 理解