深蓝黑背景封面。左侧示意图:上方左右两张卡片对比同一份业务数据的两种存法——左卡片「关系模型」内画一张两行三列的用户表格,注「多表存储,查询靠 Join 拼装」;右卡片「文档模型」内画一段嵌套 JSON(订单数组内嵌在文档里),注「一次读回整棵聚合」,两卡之间以箭头相连;下方标签「选型坐标系的四个维度」引出四枚圆角胶囊:Schema 自由度、事务需求、查询模式、运维成本。右侧大标题「关系型不够用的时候,文档数据库什么时候能赢」,副标题「文档模型 vs 关系模型:四个维度做选型」。左上标注「知识延伸 · 数据库」,右上圆角胶囊标注「Day 01 · 11-08」 图 1:本文封面,两种模型的同一份数据与选型四维度。

同批的两篇 MongoDB 真题分别讲了分片(数据超单机后的水平拆分)和副本集(高可用)的机制细节。这篇退一步,回答一个更靠前的问题:在选型阶段,凭什么判断一个业务该用文档数据库而不是关系型? 答案不是「数据结构灵活」一句话,而是一套可以逐维度打分的坐标系。

建模差异:同一份数据的两种摆法。

关系模型的信条是规范化:把实体拆成多张小表,消灭冗余,查询时再用 Join 拼回业务对象。文档模型的信条恰恰相反——MongoDB 官方数据建模文档把核心原则概括为「一起访问的数据应该一起存储」:把一笔订单连同它的明细行、收货地址内嵌成一个文档,读的时候一次取回整棵聚合。

两种摆法没有对错,只有与访问模式的匹配度。订单页要展示「订单加明细」,文档模型一次读搞定;财务月底要做「按商品维度汇总全量订单」,规范化的明细表加索引才是正道。选型的第一步永远是先列出系统的读写模式清单,再看数据该按什么边界组织。

用代码对比两种读法。

下面这段脚本把同一笔订单按两种模型各存一份,统计「读出一整棵订单」需要的读次数,并演示文档模型的 Schema 演进。用 Node.js 22.15.0 运行(node modeling.js):

// 关系模型:订单与明细两张表,读一整棵订单要查两张表再拼装
const orders = [{ id: 1, user: '小明', status: 'paid' }];
const items = [
  { orderId: 1, sku: 'A-100', qty: 2 },
  { orderId: 1, sku: 'B-200', qty: 1 },
];
function readOrderRelational(id) {
  let reads = 0;
  const order = orders.find(o => o.id === id); reads++;         // 读订单表
  const lines = items.filter(it => it.orderId === id); reads++; // 读明细表
  return { reads, doc: { ...order, items: lines } };
}

// 文档模型:整棵聚合内嵌在一个文档里
const orderDocs = [{
  _id: 1, user: '小明', status: 'paid',
  items: [{ sku: 'A-100', qty: 2 }, { sku: 'B-200', qty: 1 }],
}];
const r = readOrderRelational(1);
console.log(`关系模型:读一整棵订单需要 ${r.reads} 次读(两张表 Join 拼装)`);
console.log(`文档模型:读一整棵订单需要 1 次读(一次取回整棵聚合)`);

// Schema 演进:新文档直接多一个字段,旧文档无需迁移
orderDocs.push({ _id: 2, user: '小红', status: 'paid', coupon: 'NEWUSER', items: [] });
console.log('新文档多一个 coupon 字段:', JSON.stringify(orderDocs[1]));

实测输出:

关系模型:读一整棵订单需要 2 次读(两张表 Join 拼装)
文档模型:读一整棵订单需要 1 次读(一次取回整棵聚合)
新文档多一个 coupon 字段:{"_id":2,"user":"小红","status":"paid","coupon":"NEWUSER","items":[]}

两个信息点:整棵聚合的读取次数从「表数量」降到 1;新增字段只影响新文档,旧文档原地不动——这就是「Schema 自由」的实际含义。

选型坐标系:四个维度逐一打分。

第一个维度是 Schema 变化频率。业务字段三天两头变(内容、商品属性、埋点事件),每次变更都要 ALTER TABLE 的关系型会成为迭代瓶颈;文档模型按需增减字段。反过来,结构高度稳定的领域,规范化 schema 反而是防止脏数据的护栏。

第二个维度是 事务需求。这是最容易被误判的一维。MongoDB 单文档写是原子的,多文档事务也支持(可以跨集合、跨分片),但官方文档自己强调:分布式事务的性能代价大于单文档写,事务的存在不应替代良好的 schema 设计——内嵌建模本来就是为了减少多文档事务的需求。所以判断标准是:跨实体的强一致操作是主路径还是边角料?是主路径(核心交易、资金流转),关系型成熟的事务体系更稳;是边角料,文档模型内嵌加单文档原子性就够了。

第三个维度是 查询模式。读写以整棵聚合为单位(按 id 取整单、整体更新),文档模型赢;大量跨实体 Join、多维聚合报表、即席查询,关系型的优化器与 SQL 生态赢。还要算上水平扩展:单表大到必须拆分是常见拐点,文档数据库把分片做成了一等公民(本批真题讲的 mongos 加分片机制),关系型则要借助中间件或应用层方案。

第四个维度是 运维成本与团队生态。文档模型的自由是把双刃剑:没有 schema 护栏,脏数据要靠自己写校验规则(MongoDB 支持对部分字段加 schema 验证);团队生态如果在关系型上沉淀深厚,切换的学习与迁移成本要计入总账。

深蓝黑背景概念图,分三个区域。左上「建模差异:同一份订单数据」:左侧关系型把数据拆成 orders 表(id、用户)与 items 表(订单 id、商品)两张表,查询时 Join 拼装;箭头指向右侧文档型——一个 JSON 文档把用户与 items 数组整体内嵌,一次读操作取回整棵聚合。右上「选型四象限」:横轴为关联与事务需求(左低右高),纵轴为 Schema 变化频率(下低上高);左上象限标「文档数据库甜区:内容、目录、日志、IoT」,右下象限标「关系型主场:核心交易、财务、报表」,右上与左下为灰区,看团队与生态。底部左右两块清单:左「信号:文档数据库更合适」(数据结构频繁变、读写以整棵聚合为单位、需要水平扩展、跨实体强事务很少);右「信号:留在关系型」(跨表强事务、复杂 Join 与报表、结构稳定且规范化、团队生态在关系型) 图 2:建模差异对照与选型四象限,底部是两组信号清单。

把四维度收成两组信号。

文档数据库更合适的信号:数据结构频繁变化、读写以整棵聚合为单位、需要水平扩展、跨实体强事务很少——内容管理、商品目录、日志与 IoT 数据、用户画像都落在这一区。留在关系型的信号:跨表强事务是主路径、复杂 Join 与报表是日常、结构稳定且受益于规范化、团队生态在关系型——核心交易、财务系统、BI 报表是典型主场。中间地带的灰区,往往团队熟悉度比理论优劣更决定成败。

值得强调的是,「关系型不够用」很少指关系型本身不行,更多是指某一种访问模式在关系模型下代价过高:要么是整棵聚合的反复 Join,要么是频繁的结构变更,要么是单表扩展瓶颈。先把瓶颈定位到维度上,再谈换不换库。你所在的团队做过关系型与文档数据库之间的选型或迁移吗?当时是哪个维度成了决定性因素?欢迎分享你的判断过程。