MongoDB 模型、聚合与复制边界:7.0.45 standalone 使用观察
前一篇讲清了 MongoDB 的源码机制:文档模型、索引、复制与恢复固定 7.0.14 源码讲 RecordId/KeyString/唯一索引/条件更新/数组语义/journal・oplog・rollback/事务错误标签。本页是 U55(#92)的 E02 使用观察页:在本机 MongoDB 7.0.45 standalone(DBngin,127.0.0.1:27017)上实跑 8 组模型、聚合与超时/容量实验,并把 standalone 与复制集要求分开验收。
证据等级:全部观察来自真实服务(labs/mongo-usage/run.py,库 u55_<run-id> 结束删除,原始观察见运行证据);客户端是实验自带的最小 BSON + OP_MSG 实现(本机没有 mongosh/pymongo),只覆盖本实验所需命令。所有数字是单次观察。复制集/分片/事务提交未运行——按票面「受阻仍算待验证」登记,不算通过。
一句话主线:文档模型的硬边界(16MB、元素语义)与复制集边界(事务/change stream)是两套不同的失败,不能互相代偿。
模型硬边界:16MB 与元素语义
16MB 文档上限
插入一个约 17MB 的文档被服务端直接拒绝:code=10334 BSONObjectTooLarge(U55 docSizeLimit=PASS rejected=True code=10334)。这个上限写在源码常量里(固定 r7.0.14,src/mongo/bson/util/builder.h 64–76 行):
with any object that fits in ram.
Also note that the server has some basic checks to enforce this limit but those checks are not
exhaustive for example need to check for size too big after
update $push (append) operation
*/
const int BSONObjMaxUserSize = 16 * 1024 * 1024;
/*
Sometimes we need objects slightly larger - an object in the replication local.oplog
is slightly larger than a user object for example.
*/
const int BSONObjMaxInternalSize = BSONObjMaxUserSize + (16 * 1024);三条工程含义:内嵌文档的上限是单文档,不是集合;"无限增长的内嵌数组"迟早撞线($push 的检查按注释也不是穷尽的);超限的正确出路是拆引用或分桶(bucket pattern),不是调参数。
点路径跨元素 vs $elemMatch 同元素
同一组订单数据(items 数组)上两种查询的真实结果:
{"items.sku":"A","items.qty":1}(点路径)→ 1 条。两个条件可以分别由不同数组元素满足(sku=A 在元素 1,qty=1 在元素 2)。{"items":{"$elemMatch":{"sku":"A","qty":1}}}→ 0 条(没有任何单个元素同时满足)。$lookup关联 customers 后取customer.name→ 甲,引用模型需要显式 join 才能拿到跨表信息。
这就是"文档模型省掉 join、但要自己承担元素语义"的实感:数组内条件必须用 $elemMatch 才能表达"同一元素",点路径表达的是"任意元素"。设计文档边界时,"订单行"这样的数组天然适合同元素约束。
聚合:谓词位置决定执行计划
同样对 500 条 events(kind=buy/click,kind 建了索引):
| pipeline | 计划 | 检查文档 | 检查索引键 | 结果组数 |
|---|---|---|---|---|
$match:{kind:"buy"} → $group | IXSCAN | 100 | 100 | 1 |
$group(按累加器 total 过滤)后 $match:{total:{$gt:100}} | COLLSCAN | 500 | 0 | 2 |
(U55 aggregationPushdown=PASS matchFirstExamined=100 matchFirstKeysExamined=100 matchFirstPlan=IXSCAN matchAfterGroupExamined=500 matchAfterGroupPlan=COLLSCAN earlyGroups=1 lateGroups=2。)
关键在于能下推的 $match 才会走索引:$match 在 $group 之前且条件只涉及原始字段时,优化器把它下推到 $cursor,用 IXSCAN;$match 在 $group 之后、且条件依赖累加器(total 是新产生的字段)时,无法下推,必须先全表扫描分组再过滤。$match 是每个 $group 管道的成本分水岭——把过滤条件尽量前置,是聚合性能的第一原则。
$group 之后的累加器过滤也有正确写法:能改成 $match 前移就前移;确实要按聚合结果过滤,就让 $group 的输入最小化(先在集合上 $match 缩小候选),而不是把过滤全压在最后。
$jsonSchema:写时校验,不是迁移
给 accounts 集合加 $jsonSchema(balance 必须 double、tier 枚举):
- 合法文档(
balance:10.0)插入成功,无 writeErrors。 balance:"10"(字符串)插入 → writeErrors code=121 Document failed validation,但命令本身ok:1——校验失败是写入错误,不是命令失败(U55 schemaValidator=PASS invalidWriteErrors=1 code=121)。
三条工程含义:
- 校验是写时行为。集合已经有的脏数据不会因为加校验而消失;
validationLevel:"strict"只管新写。要做历史数据清洗是另一项迁移工作。 - 命令层
ok:1不代表写入成功。驱动必须检查writeErrors,否则会静默丢写——insertMany部分失败就是这样表现的。 - schema 演进靠校验 + 索引 + 应用层语义三者配合,不是单一
$jsonSchema能兜住的;跨字段约束(如"balance 与 tier 一致")它表达不了。
索引前后:计划形态变了,不只是变快
orderId 建索引前:COLLSCAN;建索引后:IXSCAN(U55 indexPlan=PASS beforeIndexStage=COLLSCAN afterIndexStage=IXSCAN)。看执行计划的形态变化比看单次耗时(本机毫秒级、噪声大)更能证明索引被用上。
standalone 与复制集:三组边界分开验收
本机只有 standalone,这本身就是可观察的事实(hello.replicaSet=False)。
| 能力 | standalone 上的真实结果 | 结论 |
|---|---|---|
| 多文档事务 | startTransaction 被拒,code=20「Transaction numbers are only allowed on a replica set member」;文档没有写入 | 事务要求复制集,standalone 无法验收 |
| change stream | $changeStream 聚合被拒,code=40573 | 复制集专属能力,standalone 无法验收 |
| 复制集状态 | replSetGetStatus 被拒,code=76 | 拓扑查询都需要复制集 |
(U55 transactionBoundary=PASS ... code=20 insertedAnyway=0;U55 changeStreamBoundary=PASS changeStreamRejected=True code=40573 replSetGetStatusRejected=True code=76。)
「受阻仍算待验证」:这三项在 standalone 上的"被拒绝"是真实观察,但事务正确性(提交、回滚、重试、错误标签)本身没有验证——需要复制集拓扑。机制页的事务错误标签与 change stream 恢复窗口讲解保留,不因为本页没跑就当成已验收。
容量数字:stats 的边界
dbStats / collStats 单次观察:objects=505、dataSize≈24.7KB、indexSize≈56KB、orders storageSize=4096、totalIndexSize=24576(U55 storageStats=PASS)。三条读法:这些数字含空闲页与压缩差异,不是"数据实际大小";单次观察不能当容量基线;索引尺寸经常超过数据本身(本例 indexSize > dataSize),是小集合上建索引的常见现象。
未运行与受阻(如实登记)
| 项目 | 状态 |
|---|---|
| 复制集、分片、事务提交/回滚/重试 | 未运行(本机只有 standalone) |
| change stream 实际运行与恢复窗口 | 未运行(同上) |
| 容量增长/压缩对比、批量写入吞吐 | 未运行 |
| 7.0.45 与 7.0.14 差异逐项核对 | 未运行(既有教材固定 7.0.14 源码) |
实验读法
python3 labs/mongo-usage/run.pydocSizeLimit:16MB 硬边界(源码常量摘录在此页)。embeddedVsReferenced:点路径跨元素 vs$elemMatch同元素 vs$lookup。aggregationPushdown:$match位置改变 IXSCAN/COLLSCAN。schemaValidator:$jsonSchema写时校验 code=121。indexPlan:COLLSCAN → IXSCAN。transactionBoundary/changeStreamBoundary:standalone 拒绝,复制集专属。storageStats/cleanup:容量数字与定向清理。
闭卷验收
本页 13 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。