Skip to content

第 10 章:关系系统与文档系统

最后一章转换视角。我们不再增加机制,而是向关系微型系统与本文档微型 系统提出同一组工程问题:存储什么?名字怎样绑定?匹配在哪里发生? 索引拥有什么?哪些 operator 会阻塞?原子边界在哪里?目标不是宣称 某个模型更优,而是学会哪些假设可以安全迁移,哪些必须重新推导。

学习目标

学完本章,你将能够:

  • 比较 schema-bound row 与 self-shaped nested document;
  • 把 SQL binding/关系 plan operator 对应到点路径 matching 与文档 pipeline stage;
  • 对照 primary/unique index 与 MiniMongoDB 强制 _id、multikey ownership;
  • 准确陈述 MiniMongoDB 的单文档和 batch 持久边界;
  • 使用仓库 mapping 与 differences ledger,区分可迁移机制与教学简化。

10.1 存储模型:先看形状,再看算子

关系系统把 tuple 存在有声明 schema 的 relation 中。列名、类型、 nullability 与 constraint 在查询查看 row 前就具有 catalog 含义。 文档系统存储自带嵌套形状的 record。同一 collection 的两个文档可以有 不同字段或数组长度,所以 path resolution 与 missing value 仍是运行时 问题。

MiniMongoDB 在 src/minimongodb/bson/types.py 把这点具体化。 clone_document 验证并 deep-copy 支持的 Python dict/list tree; type_tagbson_equalcanonical_keybson_compare 在没有 collection schema 时建立值身份与顺序。在 src/minimongodb/bson/path.py 中,get_path 动态解释点组件,遍历 字典与明确数组位置,路径不存在时返回 MISSING

关系侧对应物是绑定后的 column reference:完成 parse/bind 后,operator 知道读取哪个 tuple slot 与声明类型。MiniMongoDB 的 "customer.address.city" 仍是逐文档计算的 value-level path。这种 灵活性适合演进的嵌套形状,却把部分失败与歧义从定义期移动到查询/ 更新时间。

Identity 也不同。关系表可声明 primary key 与额外 unique constraint。 每个 MiniMongoDB Collection 都在 Collection.__init__ 构造 IdIndexCollection.insert_many 用注入 generator 填充缺失 _id。 自动索引是强制的,而微型系统甚至允许结构化 _id,因此需要 canonical BSON-shaped key,而不是 scalar tuple column。

10.2 查询语言与 operator flow

关系路径通常是:

SQL text → parser → binder/catalog → logical plan → physical operators

Predicate 消费 schema-bound expression;Volcano-style executor 让父 operator 从子节点 pull tuple。Selection、projection、join、aggregate、 sort 形成 plan tree。

MiniMongoDB 从 parse 之后开始,因为 API 已接收 Python query document。 src/minimongodb/collection.pyCollection._run_querymatches 验证 query document,调用 choose_plan,取得所有文档或索引候选 id, 最后再用 matches 过滤:

query dict → IXSCAN/COLLSCAN candidate choice → matcher → cloned result list

较短的 frontend 不代表语义简单。 src/minimongodb/query/matcher.pymatches 拥有 array element fan-out、literal whole-array/document equality、dotted selection、 missing/null distinction 和支持的 logical/comparison operator。这些 运行时值规则就是文档侧对关系 binding 与三值 predicate 语义的对应。

Aggregation 又增加 operator pipeline。 src/minimongodb/aggregate/__init__.pyexecute_pipeline 组合文档 iterable。$match/$project/$limit 增量 pull,$sort/$group 阻塞并 物化。关系 Filter/Project 与文档 $match/$project 共享逐 record 转换思想;关系 Sort/Aggregate 与文档 $sort/$group 共享内存 边界。区别在被转换的 record:一个是列已绑定的固定 tuple,一个是用 点引用的自描述嵌套文档。

MiniMongoDB 没有 join stage。关系 normalized design 可能把 order 与 line item 分表再 join;文档设计可嵌入 line-item subdocument,使一个 order 能被整体读取,代价是重复与可能更大的 update unit。真实 MongoDB 提供 $lookup,本微型系统没有。

10.3 索引:row key 与 document fan-out

两个模型都用索引把有序 key 映射到 record identity,有序复合键也都依赖 最左前缀。MiniMongoDB 的 SecondaryIndex.prefix_lengthchoose_plan 让这个共同机制可见。

文档模型增加了 multikey ownership。 src/minimongodb/index/secondary.pySecondaryIndex._document_entries 遍历点路径,递归展开数组 leaf, 计算复合笛卡尔积。一个文档因此可拥有多个 index key。传统 scalar 关系索引通常每 row 一个 key tuple;关系式表示 tags 往往使用 child table,每个 tag 对应一行和一条 index entry。

这一区别改变 cardinality 推理。MiniMongoDB 的 keysExamined 数匹配 key bucket,docsExamined 数候选 owner。多个 key 可指向一个文档, 一个 key 也可指向多个文档。normalized child table 的规划器则推理 child row 以及回到 parent 的 join。

两个世界的 uniqueness 都应前瞻:发布前拒绝冲突 key。MiniMongoDB 在 SecondaryIndex.validate_documentsvalidate_replace 实现它。 但 missing 规则是项目特定的:missing 占类 null key,所以 unique index 允许一个 missing/null owner。不要未经核实就把这个结果迁移到 SQL NULL uniqueness 或每种 MongoDB index option。

10.4 更新、日志与事务边界

关系系统通常暴露跨多个 statement/row 的 transaction,并带 isolation 与 rollback 规则。WAL 记录崩溃恢复需要的 storage change;生产数据库 还可能暴露 logical decoding 或 replication record。

MiniMongoDB 的边界更小。Collection._update 构造并验证副本,持久 emit 一条 post-image oplog entry,再用 Collection._replace_at 交换 文档及索引。单文档 mutation 只在一个 Python 进程内 atomic。它没有 并发读写隔离、MVCC、session、read/write concern 或多文档 transaction。

Batch 方法不会扩大边界。insert_many 整批验证 candidate,却逐文档 emit 与 publish。update_many/delete_many 同样逐文档 commit。journal 失败会留下更早的 durable prefix,并保持后续 item 未变。这与把关系 多行 statement 包在一个 ACID transaction 内差别显著。

日志设计也不同。Collection._post_image_update$inc 等动作改写 为最终 $set 值。Oplog.emit 在发布序号前把逻辑记录发给 journal listener。真实 MongoDB 分离 WiredTiger recovery journal 与 replica-set oplog;MiniMongoDB 有意用同一逻辑记录为本地恢复做 frame。

src/minimongodb/database.pyDatabase.__init__ 加载 checkpoint 与 journal state,恢复文档/索引定义,并只在 checkpoint sequence 后 调用 replayDatabase.checkpoint 写完整 database snapshot 与当前 durable sequence。这与许多系统“snapshot + newer log”的宽泛恢复形状 相似,却不是 page-oriented relational WAL 或 WiredTiger checkpoint。

10.5 实用对照矩阵

问题 关系微型系统 / MiniPostgres 视角 MiniMongoDB
存储单位 Relation 中的 schema-typed row Self-shaped nested document
名称解析 绑定后的表/列引用 运行时点路径
查询表面 Parsed SQL expression Python query document / MQL 子集
物理访问 行/索引上的 scan operator COLLSCANIXSCAN 候选
组合方式 Plan tree,父节点 pull tuple 有序文档 stage pipeline
重复数据 Child table 产生更多 row Array 产生 multikey fan-out
Identity 声明 PK/UNIQUE 强制自动 _id_
阻塞工作 Sort/aggregate 拥有输入 $sort/$group 拥有输入
Update unit 由 transaction 管理 每条 durable entry 一个复制文档
恢复日志 面向关系 storage 的 WAL 带 frame 的逻辑 post-image entry

该矩阵是阅读指南,不是 compatibility claim。用它提出相同 ownership 问题:谁验证值?谁选择候选?谁拥有中间状态?何时变更持久?什么可以 重试?

10.6 与真实系统对照

真实 PostgreSQL 家族系统还加入 catalog、SQL parsing/binding、join、 constraint、复杂 optimizer、MVCC、lock、transaction isolation、page storage、WAL、vacuum、replication 等。真实 MongoDB 还加入 binary BSON、 wire protocol/driver、storage-engine page/cache/checkpoint、replica set、 sharding、transaction、concern、丰富 MQL/aggregation 与大量索引类型。

MiniMongoDB 是 deterministic single-process kernel,不是 server。它没有 wire protocol、authentication、concurrent isolation、multi-document ACID、replication、sharding、$lookup 或完整 BSON/MQL。它最有力的 课程是本地机制:BSON-shaped equality、path/array matching、canonical multikey ownership、candidate planning、post-image convergence 与 durability-before-publication。

推广任何行为前先查阅 与 MongoDB 的差异,再使用 关系算子与文档管道映射表的级别标签:“等价”命名一个 invariant, “有意简化”缩小 machinery,“语义相反”则警告不要类比。这种证据纪律 正是 System-in-Miniature 系列要教授的方法。

10.7 动手实验:用三种方式读取一个嵌入形状

运行:

UV_CACHE_DIR=/tmp/minimongodb-uv-cache uv run python - <<'PY'
from minimongodb import Collection
orders = Collection("orders")
orders.insert_many([
    {"_id": 1, "customer": {"name": "Ada"}, "items": [{"sku": "PEN"}, {"sku": "BOOK"}], "total": 12},
    {"_id": 2, "customer": {"name": "Lin"}, "items": [{"sku": "PEN"}], "total": 4},
])
print("dotted query:", [d["_id"] for d in orders.find({"customer.name": "Ada"})])
print("array fan-out:", [d["_id"] for d in orders.find({"items.sku": "PEN"})])
print("grouped:", orders.aggregate([{"$group": {"_id": None, "revenue": {"$sum": "$total"}}}]))
PY

实测输出:

dotted query: [1]
array fan-out: [1, 2]
grouped: [{'_id': None, 'revenue': 16}]

关系表示通常把 orders/items 分开,用 schema 绑定 column,并对 row 做 join 或 aggregate。该输出并不证明哪种布局更好,只让 embedded document 的运行时 path 与 array 语义可见。

运行跨机制合同:

UV_CACHE_DIR=/tmp/minimongodb-uv-cache uv run pytest -q tests/test_query.py tests/test_aggregate.py tests/test_indexes.py

实测输出:

................................                                         [100%]
32 passed in 0.13s

这些命令不使用 socket,只验证 MiniMongoDB 行为。若要与 live PostgreSQL 或 MongoDB service 对比,则属于“需运行时验证”,本文没有声称已验证。

10.8 练习

理解题 1:建模 tags

比较 embedded tags array 与关系 article_tags child table。重复出现 在哪里?index entry 标识什么?

参考答案

文档模型中,重复位于一个文档内,multikey index 把每个展开 tag key 映回同一文档 _id。normalized 关系模型中,重复位于独立 child row;索引标识这些 row,到达 article 可能需要 join。

理解题 2:原子性声明

insert_many 成功通过 validation,能否描述为 multi-document transaction?

参考答案

不能。validation 是 batch-wide,但 durable emit 与 publication 逐文档发生;失败会保留已提交前缀。没有 rollback、isolation 或 multi-document ACID 边界。

动手题 3:建立关系式 projection

不改变 src/,写临时脚本,把实验中的 embedded orders 转为扁平 (order_id, customer_name, sku) tuple。验收:输出按确定顺序包含三条 tuple,order 1 保留两行。

参考答案

一种直接 API 解法:

rows = [
    (order["_id"], order["customer"]["name"], item["sku"])
    for order in orders.find()
    for item in order["items"]
]
assert rows == [
    (1, "Ada", "PEN"),
    (1, "Ada", "BOOK"),
    (2, "Lin", "PEN"),
]

comprehension 执行 application-side unnest analogue。真实关系执行与 MongoDB $unwind 有更丰富的规划与语义;验收只检查 shape transform。

小结

关系与文档系统共享 typed identity、candidate access、operator composition、blocking state、uniqueness、durable log 等深层机制,却把 它们放在不同 record shape 与 atomicity contract 周围。MiniMongoDB 的 价值在于每条边界都能从 Python document 一直追踪到 matching、indexing、 planning、post-image logging 与 recovery。带到真实系统中的持久习惯是: 迁移类比前,先检查 ownership 与 evidence。