如何设计 SQL 执行器
SQL 不是直接执行的字符串,它要被拆成一棵可运行的算子树。
这一页只讲一件事:MiniDB 如何把 SELECT * FROM users WHERE id = 1 变成真正的扫描、过滤、投影和返回结果。
table.scan(row -> row.id == 1)
-- 用户想写的是:
SELECT * FROM users WHERE id = 1;
| 问题 | 长出的设计 |
| SQL 字符串不知道结构。 | Parser 把 SQL 变成 AST。 |
| 字段名可能不存在。 | Binder 绑定表、列、类型。 |
| AST 不能直接跑。 | Planner 生成逻辑计划。 |
| 逻辑计划还没有执行方式。 | Executor 用算子拉取 tuple。 |
SQL
-> Parser: SelectStmt(table=users, where=id=1)
-> Binder: users.id is int
-> Planner: Projection(Filter(SeqScan(users)))
-> Executor: next() -> tuple
| 算子 | 负责什么 |
| SeqScan | 从表页按 RID 顺序读 tuple。 |
| IndexScan | 从 B+Tree 找到 RID,再回表取 tuple。 |
| Filter | 判断 where 条件。 |
| Projection | 只输出需要的列。 |
| Insert | 写 heap page,并维护索引。 |
| Update | 找到旧 tuple,写新 tuple 或原地更新。 |
| Delete | 标记删除,并维护索引可见性。 |
| 边界 | 为什么重要 |
| 不能绕过事务。 | Insert / Update / Delete 必须挂到当前 transaction。 |
| 不能忘记索引。 | 写表页和写索引页要保持一致。 |
| 不能自己决定快慢。 | 执行器跑计划,优化器选计划。 |
| 不能返回脏 tuple。 | 读到什么要服从锁或 MVCC 可见性。 |
| 实验 | 通过标准 |
| 查询不存在字段。 | Binder 报错,不进入执行器。 |
| SELECT 指定列。 | Projection 只返回指定列。 |
| WHERE id = 1。 | 能先用 SeqScan 跑通,再接 IndexScan。 |
| UPDATE 后再 SELECT。 | 结果一致,索引不指向旧值。 |
| 事务中 UPDATE 后 ROLLBACK。 | 执行器写入可被事务系统撤销。 |
一句话:SQL 执行器负责把计划树真正跑起来;它不负责“选最优计划”,但必须保证每个算子读写的数据都符合事务、索引和可见性规则。