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

4.9 KiB
Raw Blame History

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
mongodb
MongoDB
文档模型
嵌入
引用
复合索引
ESR
聚合管道
事务
变更流
副本集
basic
backend.postgresql.index
3
backend.mongodb.scenario.conversation-store
backend.elasticsearch.index
backend.mongodb.interview
14
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 支持事务吗?有什么限制?