Files
zhuangang/topics/backend/mongodb/01-document-model.md
T

106 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: backend.mongodb.document-model
title: MongoDB 基础:文档建模(嵌入还是引用)、索引与 ESR、聚合、事务和变更流
type: lesson
domain: backend
stack: [mongodb]
tags: [MongoDB, 文档模型, 嵌入, 引用, 复合索引, ESR, 聚合管道, 事务, 变更流, 副本集]
level: basic
prerequisites: [backend.postgresql.index]
est_hours: 3
related: [backend.mongodb.scenario.conversation-store, backend.elasticsearch.index, backend.mongodb.interview]
week: [14]
order: 1
---
# MongoDB 基础:文档建模(嵌入还是引用)、索引与 ESR、聚合、事务和变更流
> BOSS直聘 2026-10-23 的调研中,有岗位把 MongoDB 列为要求或加分项(常见于存储对话记录、日志、灵活结构的业务数据)。定位:**了解并能用**,主库仍以 PostgreSQL / MySQL 为主。示例基于 MongoDB 8.2 + PyMongo 4.19 实际运行通过;以官方文档为准:https://www.mongodb.com/docs/manual/ 。
## 解释说明
### 和关系型数据库的对应
| 关系型 | MongoDB |
| --- | --- |
| 数据库 / 表 / 行 | database / collection / document(BSON,类似 JSON) |
| 表结构 | 默认无固定结构,可用 `$jsonSchema` 校验 |
| JOIN | 优先“嵌入”,必要时用聚合的 `$lookup` |
| 事务 | 单文档写入天然原子;多文档事务需要副本集或分片集群 |
### 建模:嵌入还是引用
- **嵌入**:总是一起读写、数量有上限的子数据(会话中的最近消息、订单中的商品快照)。一次读取拿到全部,单文档原子更新。
- **引用**:数量无上限或会被独立访问、多处共享的数据(用户、商品主数据、海量消息)。
- 单个文档上限 16MB,数组不能无限增长(“无限数组”是常见反模式)。
### 索引
- 单字段、复合、多键(数组)、TTL(自动过期)、唯一、文本索引等。
- **复合索引的 ESR 规则**:等值条件字段在前(Equality),排序字段居中(Sort),范围条件字段在后(Range)。
- 用 `explain()` 看是否 `IXSCAN`(走索引)、有没有 `COLLSCAN`(全表扫描)和 `SORT`(内存排序)。
### 聚合管道
`$match` → `$unwind` → `$group` → `$sort` → `$project` …… 像流水线一样处理;`$match` 尽量放在最前面以利用索引。
### 副本集、事务、变更流
- **副本集**:一主多从,自动故障转移;读写关注(`readConcern` / `writeConcern`)控制一致性。
- **事务**:多文档 ACID,需要副本集;有性能成本,优先通过建模让一次写入只改一个文档。
- **变更流(Change Streams)**:订阅集合的变更,常用于同步到 ES / 缓存 / 消息队列,支持 resume token 断点续传。
## 案例
AI 客服会话存储:会话文档嵌入最近消息(上限 200 条),结构随渠道不同而变化;按“租户 + 状态 + 时间倒序”查询待处理会话;转人工时同时改会话状态并创建工单(事务);变更流把工单同步到搜索服务。
## 代码示例
[examples/conversations/mongo_demo.py](examples/conversations/mongo_demo.py)(需要 `MONGO_URL`,单节点副本集即可),实际输出:
```text
1. Schema 校验:非法 status 被拒绝
2. 查询走索引且无需内存排序:['FETCH', 'IXSCAN', 'LIMIT']
3. 聚合统计 6 组,例如 {'tenant': 't0', 'channel': 'app'} 共 50 个会话,平均 22.9 tokens
4. 幂等 upsert:重复投递 3 次只有 1 条
5. 事务:成功的提交生效,失败的整体回滚
6. 变更流收到插入事件,并拿到 resume token 用于断点续传
```
ESR 索引和 explain:
```python
conv.create_index([("tenant_id", 1), ("status", 1), ("created_at", -1)]) # 等值、等值、排序
plan = conv.find({"tenant_id": "t1", "status": "handoff"}).sort("created_at", -1).limit(10).explain()
```
幂等 upsert:
```python
events.create_index("event_id", unique=True)
events.update_one({"event_id": "evt-001"}, {"$setOnInsert": {"payload": "退款申请"}}, upsert=True)
```
本地启动单节点副本集(无 Docker 时可用官方二进制包):
```bash
mongod --dbpath ./data --replSet rs0 --bind_ip 127.0.0.1 --port 27017
mongosh --eval 'rs.initiate()' # 或用 PyMongo 执行 replSetInitiate
```
## 易错点
- 把关系型的表结构原样搬过来,到处 `$lookup`,失去文档模型的优势。
- 无限增长的嵌入数组。
- 以为“无结构”就不需要校验:关键字段要用 Schema 校验和应用层校验。
- 复合索引顺序不对,查询仍然内存排序。
- 单机(非副本集)部署上使用事务或变更流,直接报错。
- 用 `skip` 做深分页:越往后越慢,改用“基于上一页最后一个值”的范围查询。
## 面试常问
- 什么时候选 MongoDB,什么时候选 PostgreSQL?
- 嵌入和引用怎么选?
- 复合索引的设计原则?
- MongoDB 支持事务吗?有什么限制?