4.9 KiB
4.9 KiB
id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
| id | title | type | domain | stack | tags | level | prerequisites | est_hours | related | week | order | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend.mongodb.document-model | MongoDB 基础:文档建模(嵌入还是引用)、索引与 ESR、聚合、事务和变更流 | lesson | backend |
|
|
basic |
|
3 |
|
|
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(需要 MONGO_URL,单节点副本集即可),实际输出:
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:
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:
events.create_index("event_id", unique=True)
events.update_one({"event_id": "evt-001"}, {"$setOnInsert": {"payload": "退款申请"}}, upsert=True)
本地启动单节点副本集(无 Docker 时可用官方二进制包):
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 支持事务吗?有什么限制?