Skip to content

一条查询的内核漫游

Language: English | 简体中文

这份导览从 Database.execute() 接到一条 SQL 开始,沿真实执行路径走到页面、 事务与 WAL,再回到维护机制。每一站都用以下三档说明它和 PostgreSQL 的关系:

  • 等价:保留同一个核心契约或算法形状;不表示接口、文件格式逐字兼容。
  • 有意简化:机制方向相同,但缩小了语法、并发、统计或持久化边界。
  • 语义相反:为了让教学证据更直接,采取了与 PostgreSQL 相反的策略。

0. 总路线

SQL text
  -> sql/ lexer + parser + binder
  -> planner/ logical rules + cost optimizer
  -> executor/ Volcano tree
  -> storage/indexed.py
       -> storage/heap.py
       -> index/btree.py
       -> storage/buffer.py
  -> transaction visibility + locks
  -> wal/
  -> maintenance/ ANALYZE, VACUUM, HOT

普通查询按前三站向下拉取行。写查询还会进入 MVCC、锁、索引维护和 WAL;之后 显式 ANALYZE/VACUUM 更新统计或回收旧版本。

1. SQL 文本成为已绑定语义

本项目: src/minipostgres/sql/lexer.pyparser.pybinder.py
PostgreSQL: src/backend/parser/scan.lgram.yanalyze.c 以及 parse_*.c
标注:有意简化

Lexer 和递归下降 Parser 只生成语法 AST;Binder 才读取 catalog,解析表/列 身份、类型、别名、聚合合法性和上下文中的 NULL。这个“parse 后再 analyze/bind”的边界对应 PostgreSQL 的 raw parse tree 到 analyzed Query。 区别是 MiniPostgres 使用冻结的小语法和 JSON catalog,不承担 PostgreSQL 的 完整类型转换、命名空间、权限与系统 catalog 语义。

2. 逻辑计划成为物理计划

本项目: src/minipostgres/planner/catalog/statistics.pymaintenance/analyze.py
PostgreSQL: src/backend/optimizer/src/backend/statistics/, 以及系统 catalog pg_statistic
标注:有意简化

planner.py 构造不可变逻辑计划,rules.py 做常量折叠、谓词下推和列裁剪, optimizer.py 再比较 SeqScan/IndexScan、NestedLoop/HashJoin,并对 2–4 张表做有界动态规划(dynamic programming, DP)连接排序。

ANALYZE 产生行数、页数、空值比例(null fraction)、不同值数量 (distinct count)、最常见值(most common values, MCV)和等深直方图。 选择率估计先使用 MCV;非 MCV 的等值质量按剩余 distinct 分摊,范围条件再用 直方图插值。这对应 PostgreSQL 从 pg_statistic 的 MCV/直方图推导选择率, 但本项目做精确全表扫描、统计类型更少、代价常量固定,也没有扩展统计和大量 PostgreSQL 扫描/连接路径。

3. Volcano 执行树拉取行

本项目: src/minipostgres/executor/base.pyfactory.pyoperators.py
PostgreSQL: src/backend/executor/execMain.cnode*.c
标注:等价

物理节点经 factory 变成执行器树。每个节点遵守 open() / next() / close(),父节点通过 next() 从子节点逐行拉取, 这就是 Volcano/迭代器(iterator)模型。EXPLAIN ANALYZE 的包装器仍保持 相同拉取契约,只累计每个节点的实际行数和耗时。

这里的“等价”是执行模型等价:PostgreSQL 的执行器以 ExecProcNode 驱动计划状态树;MiniPostgres 则使用 Python 对象和更少的节点类型。

4. DML、等待后的 EPQ 与索引协调

本项目: executor/operators.pystorage/indexed.py::replace_mvcc/delete_mvcc
PostgreSQL: src/backend/executor/execMain.cnodeModifyTable.c 中的 EvalPlanQual(EPQ)路径
标注:等价(简化版)

UPDATE/DELETE 先从执行树收集候选 TID。写者取得元组锁后,可能发现另一事务已经 提交了更新;读已提交(Read Committed)此时对最新行重新计算原 WHERE 谓词。 谓词不再成立就跳过修改。这是工作树中新实现的 EPQ 谓词重查,保留了 PostgreSQL“等待后不能盲改旧候选”的关键语义。

简化之处是它只重查冻结表达式子集和单行最新版本,不重跑 PostgreSQL 完整的 EPQ 计划、触发器、分区路由等路径。可重复读(Repeatable Read)若快照版本与 锁后最新版本不同,则抛出序列化冲突。

5. 堆页面与 MVCC 可见性

本项目: src/minipostgres/storage/heap.pystorage/slotted.pytransaction/visibility.py
PostgreSQL: src/backend/access/heap/heapam.cheapam_visibility.c 中的 HeapTupleSatisfiesMVCC
标注:等价(有意简化的布局)

堆由带校验和的 8192 字节分槽页面组成,TID 是稳定的 (page_id, slot_id)。版本头保存 xminxmax 和链后继;可见性判断先检查 创建者,再检查删除者是否在当前快照中可见。这保留了 HeapTupleSatisfiesMVCC 的核心判断顺序。

本项目的元组头、槽位格式和版本链不是 PostgreSQL 磁盘格式。尤其 root_tid() 为展示前驱关系会扫描所有版本,复杂度 O(N);真实 PostgreSQL 依靠行指针重定向和 HOT 标志位直接定位,不应把这里的扫描当成生产实现方式。

6. 缓冲池与时钟扫描

本项目: src/minipostgres/storage/buffer.pystorage/replacer.py
PostgreSQL: src/backend/storage/buffer/bufmgr.cfreelist.c 的时钟扫描(clock sweep)
标注:等价(简化版)

PageGuard 拥有一个固定(pin);被固定的帧不能淘汰。时钟指针扫过帧,有引用位 的先清位并获得第二次机会(second chance),没有引用且可淘汰的成为受害者。 脏页写出前必须先通过 WAL 刷新闸门。

PostgreSQL 使用共享缓冲区、使用计数、并发原子状态、后台写入器等生产设施; 这里是进程内、固定帧数、确定性策略,但固定、第二次机会和 WAL-before-data 三个契约方向一致。

7. B+Tree 查找、分裂与合并

本项目: src/minipostgres/index/
PostgreSQL: src/backend/access/nbtree/
标注:有意简化

第零页是元页面(metapage);内部页保存分隔键/子节点,叶页保存编码后的 键/TID 和兄弟链。插入溢出时分裂并向上提升分隔键;删除不足时先向兄弟借, 再合并,必要时收缩根。点查和范围查最后返回堆 TID 候选,执行器仍会回堆重查。

这些结构对应 nbtree,但没有 PostgreSQL 的并发页面锁协议、高键 (high key)、右链接并发遍历、去重、后缀截断、清理周期等细节,键编码也只 覆盖本项目的有界标量集合。

8. 事务状态与快照

本项目: transaction/status.pysnapshot.pymanager.py
PostgreSQL: src/backend/access/transam/clog.c(CLOG/pg_xact)与 src/backend/utils/time/snapmgr.c
标注:等价(简化版)

状态表回答 XID 是进行中、已提交还是已中止,对应 CLOG 的事务提交状态职责。 快照保存 xmax 和活动 XID 集合:读已提交每条语句刷新,可重复读从首个数据 语句起固定,对应 snapmgr 的隔离级别快照生命周期。

状态表是内存/教学持久化模型,快照也没有子事务、命令 ID、快照导出/导入、 SSI 谓词锁等完整 PostgreSQL 内容。

9. 写者锁与死锁检测

本项目: transaction/locks.pydeadlock.py
PostgreSQL: src/backend/storage/lmgr/lock.cproc.cdeadlock.c
标注:有意简化

元组键和唯一键使用先进先出(first in, first out, FIFO)排他锁。队列中每个 等待者指向所有者和更早的等待者,形成等待图(wait-for graph);检测到环时 选择最高 XID 作为确定性受害者。这对应 PostgreSQL 从锁等待关系检测死锁的 机制,但 PostgreSQL 有完整的锁模式/冲突矩阵、软边重排和不同的受害者处理。 本项目没有共享锁或 SELECT FOR UPDATE

10. WAL、检查点与恢复

本项目: src/minipostgres/wal/、堆页面 LSN、缓冲区 WAL 闸门
PostgreSQL: src/backend/access/transam/xlog.cxloginsert.cfull_page_writes
标注:语义相反(全页镜像策略)

MiniPostgres 每次堆页面变更都记录完整的变更后镜像;提交记录 fsync 后才发布 提交,恢复以页面日志序列号(log sequence number, LSN)判断是否执行 REDO, 并在非干净恢复后重建派生索引。

PostgreSQL 在检查点后对某页的首次修改附带全页镜像(full-page image, FPI), 用它修复撕裂页面;同一检查点周期内后续修改通常只需更紧凑的生理式 (physiological)WAL 记录。PostgreSQL 这样设计是因为一次 FPI 已建立该页的 检查点后基线,后续记录操作/块内变化即可重放,可显著降低 WAL、复制和恢复 I/O。MiniPostgres“每次都整页”恰好相反:更浪费空间,但让 WAL-before-data 和幂等 REDO 很容易观察。它仅执行 REDO,也没有 PostgreSQL 的完整 WAL 记录 生态。

11. VACUUM、HOT 与统计维护

本项目: src/minipostgres/maintenance/storage/indexed.py::vacuum
PostgreSQL: src/backend/access/heap/vacuumlazy.cheapam.c 中的 HOT 路径,以及 src/backend/commands/analyze.c
标注:有意简化

显式 VACUUM 从活动快照计算清理水位(cleanup horizon),先删除精确索引项, 再回收稳定槽位,并可剪掉 HOT 链中的死亡中间版本。HOT 更新要求替换版本实际 落在同一堆页面且编码后的所有索引键不变,因此索引继续指向根 TID。

PostgreSQL 的惰性清理(lazy vacuum)、HOT 重定向/死亡行指针、剪枝、可见性 映射、冻结、自动清理和统计采样要丰富得多。本项目采用同步显式维护,是为了让 “何时可回收、为何无需新增索引项”能由测试直接观察。

建议的阅读顺序

先用 examples/demo.py 跑一条带 WHERE 的 SELECT 和一条 UPDATE,再按 engine.py -> sql/ -> planner/ -> executor/ -> storage/indexed.py 追调用。 随后分别阅读 transaction/visibility.pystorage/replacer.pywal/recovery.py,最后用 LABS.md 中的并发、淘汰和崩溃实验验证上述契约。