【免费下载链接】toydb
Distributed SQL database in Rust, written as an educational project
本文是 toyDB(Rust 编写的分布式 SQL 数据库,教学项目)的 SQL 方言完整参考。toyDB 在 Raft 分布式共识与 MVCC 事务之上提供了一套贴近 PostgreSQL/SQLite 习惯的 SQL 接口,支持建表约束、索引、四类连接、聚合与 GROUP BY、子句分页以及基于快照隔离的 ACID 事务。读完本文,你将掌握 toyDB 的全部数据类型与字面量规则、运算符优先级与求值细节、十条 SQL 语句的完整语法与约束语义,以及 MVCC 事务隔离与时间旅行(AS OF SYSTEM TIME)的准确行为。
toyDB 的 SQL 引擎是一套标准的流水线:Client → Session → Lexer → Parser → Planner → Optimizer → Executor → Storage,其整体结构可参考 docs/architecture/sql.md。本文以官方 SQL 参考文档 docs/sql.md 为主体,结合 src/sql 下的源码与 src/sql/testscripts 下的测试脚本展开。
数据类型
toyDB 只支持四种标量类型(无复合类型),对应枚举sql::types::DataType,见 src/sql/types/value.rs:
| 类型 | 别名 | 说明 |
|---|---|---|
BOOLEAN | BOOL | 逻辑真值TRUE/FALSE |
FLOAT | DOUBLE | 64 位有符号浮点数,IEEE 754binary64编码,支持 10⁻³⁰⁷ 到 10³⁰⁸ 的幅值与 53 位精度(约 15 位有效数字),以及特殊值INFINITY和NAN |
INTEGER | INT | 64 位有符号整数,范围 ±(2⁶³-1) |
STRING | TEXT、VARCHAR | UTF-8 编码字符串 |
此外,特殊值NULL表示未知值,遵循三值逻辑(three-valued logic)规则。
从源码看,值本身由sql::types::Value枚举承载(Null、Boolean(bool)、Integer(i64)、Float(f64)、String(String))。有两处值得注意的底层细节:
- 存储层面的类型不可互换:浮点值(即使没有小数部分)不能存入整数列,反之亦然;写入时会按列的数据类型做严格校验(见 src/sql/types/schema.rs 的
validate_row)。注意这与下一节"比较运算符中INTEGER与FLOAT可互换"并不矛盾——互换性只发生在表达式求值阶段,而非存储阶段。 -0.0与-NaN的正向归一化:为了在 KV 存储与索引查找中保持一致,序列化时-0.0与-NaN会被当作正数处理(src/sql/types/value.rs),且代码内部NaN == NaN、NULL == NULL成立,以支持检测、索引和排序;SQL 语义下的 NULL/NaN 规则在表达式求值时另行实现(src/sql/types/value.rs)。
SQL 词法与语法
关键字
关键字是 SQL 语句中具有特殊含义的保留字,不区分大小写;若想用作标识符,必须用"引号包围。完整列表如下:
AS,ASC,AND,BEGIN,BOOL,BOOLEAN,BY,COMMIT,CREATE,CROSS,DEFAULT,DELETE,DESC,DOUBLE,DROP,EXISTS,EXPLAIN,FALSE,FLOAT,FROM,GROUP,HAVING,IF,INDEX,INFINITY,INNER,INSERT,INT,INTEGER,INTO,IS,JOIN,KEY,LEFT,LIKE,LIMIT,NAN,NOT,NULL,OF,OFFSET,ON,ONLY,OR,ORDER,OUTER,PRIMARY,READ,REFERENCES,RIGHT,ROLLBACK,SELECT,SET,STRING,SYSTEM,TABLE,TEXT,TIME,TRANSACTION,TRUE,UNIQUE,UPDATE,VALUES,VARCHAR,WHERE,WRITE
该列表与词法分析器中的sql::parser::Keyword枚举逐一对应,见 src/sql/parser/lexer.rs。
标识符
标识符是表、列等数据库对象的名称。规则如下:
- 未加引号的标识符必须以 Unicode 字母开头,后跟任意字母、数字和
_的组合,且不能是保留关键字; - 用
"引号包围的标识符可以使用关键字和特殊字符,其中""用于转义双引号本身; - 标识符一律转换为小写(引号内的保留原大小写,词法器行为见 src/sql/parser/lexer.rs)。
例如SELECT "integer" FROM t中integer因被引号包围而成为合法标识符(测试证据见 src/sql/testscripts/queries/select)。
常量
命名常量:以下关键字直接求值为常量:
FALSE:布尔假值;INFINITY:浮点无穷大;NAN:浮点 NaN(not a number);NULL:未知值;TRUE:布尔真值。
字符串字面量:用单引号'包围,可包含任意合法 UTF-8 字符。单引号必须用额外的单引号转义(即''),不支持其他转义序列。例如:
'A string with ''quotes'' and emojis 😀'词法器对字符串转义的处理见 src/sql/parser/lexer.rs。
数字字面量:纯数字序列0-9解析为 64 位有符号整数;带小数点或科学计数法的数字解析为 64 位浮点数。支持的模式为:
999[.[999]][e[+-]999]负号-是独立的前缀运算符(词法器中前导符号不作为数字的一部分,见 src/sql/parser/lexer.rs)。
表达式
表达式可用于任何需要值的位置(如SELECT的列、INSERT的值),由常量、列引用、运算符调用和函数调用构成。列引用可以是未限定的name,也可以用关系标识符加点号限定,如person.name。未限定的标识符必须无歧义——若同一名字出现在多个表中,规划器会报ambiguous column错误(src/sql/planner/planner.rs)。
SQL 运算符
逻辑运算符
AND、OR、NOT对布尔操作数施加标准逻辑运算,并遵循 SQL 三值逻辑(NULL 参与)。完整真值表如下:
AND | TRUE | FALSE | NULL |
|---|---|---|---|
TRUE | TRUE | FALSE | NULL |
FALSE | FALSE | FALSE | FALSE |
NULL | NULL | FALSE | NULL |
OR | TRUE | FALSE | NULL |
|---|---|---|---|
TRUE | TRUE | TRUE | TRUE |
FALSE | TRUE | FALSE | NULL |
NULL | TRUE | NULL | NULL |
NOT | |
|---|---|
TRUE | FALSE |
FALSE | TRUE |
NULL | NULL |
求值实现位于 src/sql/types/expression.rs,注意其中的特例:NULL AND FALSE = FALSE、NULL OR TRUE = TRUE(与真值表一致)。
比较运算符
比较运算符对同一数据类型的值进行比较,成立返回TRUE,否则返回FALSE:
INTEGER与FLOAT在比较时可互换;STRING比较使用字符串的字节值,即区分大小写,且由于 UTF-8 码点关系'B' < 'a';FALSE视为小于TRUE;- 与
NULL比较恒为NULL(即使NULL = NULL)。
二元运算符:
=:相等,如1 = 1得TRUE;!=:不等,如1 != 2得TRUE;>:大于,如2 > 1得TRUE;>=:大于等于,如1 >= 1得TRUE;<:小于,如1 < 2得TRUE;<=:小于等于,如1 <= 1得TRUE。
一元运算符:
IS NULL:检查是否为NULL,如NULL IS NULL得TRUE;IS NOT NULL:检查是否非NULL,如TRUE IS NOT NULL得TRUE;IS NAN:检查是否为浮点NAN,如NAN IS NAN得TRUE。对非浮点类型报错,但NULL IS NAN得NULL;IS NOT NAN:检查是否非浮点NAN,如3.14 IS NOT NAN得TRUE。
上述IS NULL/NAN语义在 src/sql/types/expression.rs 中实现:IS NAN作用于非浮点类型(NULL 除外)会返回can't use错误。
数学运算符
数学运算符对数值(INTEGER或FLOAT)操作数施加标准数学运算:
- 若任一操作数为
FLOAT,两个操作数都会转换为FLOAT,结果为FLOAT; - 若任一操作数为
NULL,结果为NULL; INFINITY和NAN按 IEEE 754 规范处理;- 对
INTEGER操作数,溢出、除零等失败条件会报错;对FLOAT操作数,这些情况按 IEEE 754 返回INFINITY或NAN。
二元运算符:
+:加法,如1 + 2得3;-:减法,如3 - 2得1;*:乘法,如3 * 2得6;/:除法,如6 / 2得3;^:乘方,如2 ^ 4得16;%:余数,如8 % 3得2。注意这里采用"余数"而非"模运算"语义(与 PostgreSQL 一致):结果符号与被除数相同。
一元运算符:
+(前缀):恒等,如+1得1;-(前缀):取负,如- -2得2;!(后缀):阶乘,如5!得120。
底层实现通过Value上的checked_add、checked_sub、checked_mul、checked_div、checked_pow、checked_rem完成(src/sql/types/value.rs):整数运算使用checked_*系列并显式报integer overflow错误;整数除零报can't divide by zero;阶乘只对非负整数定义(src/sql/types/expression.rs)。
字符串运算符
LIKE:将字符串与模式比较,%是任意多字符通配符,_是单字符通配符;匹配返回TRUE,如'abc' LIKE 'a%'得TRUE。实现通过正则表达式转换完成(regex::escape后将%替换为.*、_替换为.),且不支持转义_与%(src/sql/types/expression.rs)。
运算符优先级
运算符的优先级(运算顺序)如下,从高到低:
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 10 | +,-(前缀) | 右 |
| 9 | !(后缀) | 左 |
| 8 | ^ | 右 |
| 7 | *,/,% | 左 |
| 6 | +,- | 左 |
| 5 | >,>=,<,<= | 左 |
| 4 | =,!=,LIKE,IS | 左 |
| 3 | NOT | 右 |
| 2 | AND | 左 |
| 1 | OR | 左 |
优先级可用括号覆盖,例如(1 + 2) * 3。
这组优先级(基本遵循 PostgreSQL,其中IS与LIKE与=同级,类似 SQLite/MySQL)由解析器通过优先级爬升算法(precedence climbing)实现,源码与完整推导注释见 src/sql/parser/parser.rs。例如2 ^ 3 ^ 2 - 4 * 3会正确解析为(2 ^ (3 ^ 2)) - (4 * 3) = 500,因为^是右结合的。行为由测试脚本逐级验证,见 src/sql/testscripts/expressions/op_precedence,例如:
> 2 ^ 3 ^ 2 > (2 ^ 3) ^ 2 --- 512 64函数
sqrt(expr):返回数值参数的平方根。底层为Expression::SquareRoot,只接受非负INTEGER/FLOAT,NULL透传为NULL(src/sql/types/expression.rs)。
聚合函数
聚合函数对某个表达式跨行求值,可配合GROUP BY分组,并用HAVING过滤结果:
AVG(expr):数值的平均值。注意空分组下返回NULL(累加器 count 为 0 时,见 src/sql/execution/aggregator.rs);COUNT(expr):expr求值为非NULL的行数;COUNT(*)用于统计全部行;MAX(expr):按数据类型排序的最大值;MIN(expr):按数据类型排序的最小值;SUM(expr):数值之和。
聚合累加器忽略NULL值(src/sql/execution/aggregator.rs),COUNT(*)被特例化为常量TRUE计数(src/sql/planner/planner.rs)。聚合函数不能嵌套。
SQL 语句
BEGIN
开启一个新事务。
BEGIN [ TRANSACTION ] [ READ ONLY | READ WRITE ] [ AS OF SYSTEM TIME txn_id ]txn_id:一个过去的事务 ID,用于以只读方式运行时间旅行(time-travel)查询。
会话层对BEGIN的处理见 src/sql/execution/session.rs:READ WRITE且带AS OF会被拒绝(can't start read-write transaction in a given version),READ ONLY AS OF SYSTEM TIME n则调用begin_as_of(n)。
COMMIT
提交当前活动事务。
CREATE TABLE
创建新表:
CREATE TABLE table_name ( [ column_name data_type [ column_constraint [ ... ] ] [ INDEX ] [, ... ] ] ) where column_constraint is: { NOT NULL | NULL | PRIMARY KEY | DEFAULT expr | REFERENCES ref_table | UNIQUE }table_name:表名,必须是合法标识符;若同名表已存在则报错;column_name:列名,必须是合法标识符且表内唯一;data_type:列的数据类型(见数据类型一节);NOT NULL:列不允许NULL值;NULL:列允许NULL值(默认行为);PRIMARY KEY:该列作为主键,即行的主要标识符。每张表必须恰好有一个主键列,且主键必须唯一、不可为空;DEFAULT expr:为列指定默认值,当INSERT未提供该列值时代入。expr可以是任意合适类型的常量表达式,如'abc'或1 + 2 * 3。对可空列,默认值为NULL(除非显式指定);REFERENCES ref_table:该列是引用ref_table主键的外键,强制参照完整性;UNIQUE:列只能包含唯一(互不相同)的值。NULL值彼此不视为相等,因此允许NULL的UNIQUE列可以包含多个NULL。PRIMARY KEY列隐式UNIQUE;INDEX:为该列创建索引。
示例
CREATE TABLE movie ( id INTEGER PRIMARY KEY, title STRING NOT NULL, release_year INTEGER INDEX, imdb_id STRING INDEX UNIQUE, bluray BOOLEAN NOT NULL DEFAULT TRUE )底层会做完整的模式校验(src/sql/types/schema.rs):必须有且仅有一个主键;主键不可为空、不可再建二级索引;UNIQUE与REFERENCES列必须建二级索引;外键列类型必须与目标主键类型一致;可空列必须带默认值。规划器还会为UNIQUE/REFERENCES列自动补充INDEX标记(src/sql/planner/planner.rs)。测试见 src/sql/testscripts/schema/create_table。
DELETE
删除表中的行:
DELETE FROM table_name [ WHERE predicate ]删除predicate求值为TRUE的行;若不给出WHERE则删除全部行。
table_name:要删除数据的表,不存在则报错;predicate:决定哪些行被删除的表达式,必须求值为BOOLEAN或NULL,否则报错。
示例
DELETE FROM movie WHERE release_year < 2000 AND bluray = FALSE执行时从扫描节点收集主键并批量删除(src/sql/execution/executor.rs)。
DROP TABLE
删除一张表及其全部数据。表不存在时报错,除非给出IF EXISTS:
DROP TABLE [ IF EXISTS ] table_nametable_name:要删除的表。
EXPLAIN
输出给定语句的执行计划:
EXPLAIN [ statement ]会话层对EXPLAIN返回优化后的计划(Plan::build(...).optimize(),src/sql/execution/session.rs),不能嵌套EXPLAIN。
INSERT
向表中插入行:
INSERT INTO table_name [ ( column_name [, ... ] ) ] VALUES ( expression [, ... ] ) [, ... ]- 若给出列名,必须提供数量一致的值;若不给出列名,值必须按表的列顺序给出;
- 省略的列取默认值(若指定),否则报错;
table_name:目标表,不存在则报错;column_name:要插入的列,不存在则报错;expression:插入对应列的值表达式,必须是常量表达式(不能引用表列)。
执行器支持列映射与默认值填充两条路径(src/sql/execution/executor.rs):未提供的列优先取DEFAULT,无默认值时报no value given for column ... with no default。
示例
INSERT INTO movie (id, title, release_year) VALUES (1, 'Sicario', 2015), (2, 'Stalker', 1979), (3, 'Her', 2013)ROLLBACK
回滚当前活动事务。
SELECT
从表中选择行:
SELECT [ * | expression [ [ AS ] output_name [, ...] ] ] [ FROM from_item [, ...] ] [ WHERE predicate ] [ GROUP BY group_expr [, ...] ] [ HAVING having_expr ] [ ORDER BY order_expr [ ASC | DESC ] [, ...] ] [ LIMIT count ] [ OFFSET start ] where from_item is one of: table_name [ [ AS ] alias ] from_item join_type from_item [ ON join_predicate ] where join_type is one of: CROSS JOIN [ INNER ] JOIN LEFT [ OUTER ] JOIN RIGHT [ OUTER ] JOIN获取行或表达式,数据来自table_name(若给出)或直接生成。
expression:要获取的表达式(可以是简单的列名);output_name:输出列标识符,默认取列名(单列时),否则无名字(显示为?);table_name:取行的表;alias:表别名;predicate:只返回该表达式求值为TRUE的行;group_expr:分组聚合的表达式。非聚合的SELECT表达式必须引用group_expr中的列、与某个group_expr相同,或有被group_expr列引用的output_name;having_expr:只返回该表达式求值为TRUE的聚合结果(要求存在GROUP BY或聚合函数,见 src/sql/planner/planner.rs);order_expr:按此表达式排序(可以是简单的列名);count:返回的最大行数,必须是常量整数表达式;start:跳过的行数,必须是常量整数表达式;join_predicate:只返回该表达式求值为TRUE的连接行。
连接类型:
CROSS JOIN:两表的笛卡尔积,不接受连接谓词(ON子句);INNER JOIN:两表笛卡尔积中join_predicate为TRUE的行;LEFT OUTER JOIN:按join_predicate连接的行;对左表中没有匹配的每一行,返回一行右表列全为NULL的行;RIGHT OUTER JOIN:与LEFT OUTER JOIN相同,但左右表互换。
规划阶段RIGHT OUTER JOIN被实现为左右交换的LEFT OUTER JOIN加列重映射(src/sql/planner/planner.rs)。执行阶段默认使用嵌套循环连接(NestedLoopJoiner,src/sql/execution/join.rs),优化器在等值连接条件下可将其改写为哈希连接(HashJoiner,src/sql/execution/join.rs),五个优化器(常量折叠、过滤下推、索引查找、哈希连接、短路求值)的注册顺序见 src/sql/planner/optimizer.rs。
示例
SELECT id, title, 2020 - released AS age FROM movies WHERE released >= 2000 AND ultrahd ORDER BY released DESC, title ASC LIMIT 10 OFFSET 10完整的 SELECT 行为(含*展开、别名、限定/非限定列、歧义报错等)可参考 src/sql/testscripts/queries/select;GROUP BY、HAVING、LIMIT/OFFSET、ORDER BY的专项测试分别在 src/sql/testscripts/queries/group_by、src/sql/testscripts/queries/having、src/sql/testscripts/queries/limit、src/sql/testscripts/queries/order。
UPDATE
更新表中的行:
UPDATE table_name SET column_name = expression | DEFAULT [, ... ] [ WHERE predicate ]将column_name对应的列更新为expression的值,作用于predicate为TRUE的所有行;不给出WHERE则更新全部行。
table_name:要更新的表,不存在则报错;column_name:要更新的列,不存在则报错;expression:求值后写入对应行对应列的值。表达式可以引用列值,且必须与更新列的数据类型一致。使用DEFAULT则写入该列的默认值(若存在);predicate:决定哪些行被更新的表达式,必须求值为BOOLEAN或NULL,否则报错。
示例
UPDATE movie SET bluray = TRUE WHERE release_year >= 2000 AND bluray = FALSE事务与隔离级别
toyDB 使用基于 MVCC 的快照隔离(snapshot isolation)支持 ACID 事务,可防止以下异常:脏写(dirty writes)、脏读(dirty reads)、丢失更新(lost updates)、模糊读(fuzzy reads)、读偏斜(read skew)和幻读(phantom reads)。由于未实现可串行化快照隔离,写偏斜(write skew)异常是可能发生的。
- 事务用
BEGIN开启,以COMMIT(原子写入全部变更)或ROLLBACK(丢弃全部变更)结束; - 若并发事务之间发生冲突,事务 ID 较小者胜出,其余事务将因序列化错误而失败,需要重试;
- 所有历史数据都会版本化并保留,可通过
BEGIN TRANSACTION READ ONLY AS OF SYSTEM TIME <txn_id>按给定事务 ID 查询过去的数据快照(时间旅行查询); - 事务内某条语句返回错误后,该事务仍然有效,由客户端决定后续操作。
SQL 层面的事务行为由会话(Session)统一管理(src/sql/execution/session.rs):BEGIN/COMMIT/ROLLBACK直接在会话中处理;普通语句在没有显式事务时自动使用隐式事务,SELECT走只读隐式事务。会话析构时会自动回滚未结束的事务。
src/sql/testscripts/transactions/isolation 给出了一个完整的隔离行为验证脚本:事务c4开始时只看到此前已提交的c1写入与自己的写入,看不到此后才提交的c2、未提交的c3与未来的c5;而BEGIN READ ONLY AS OF SYSTEM TIME 4的c7即使在c4提交之后查询,也始终只看到版本 4 时的快照:
c4: 1, 'a' c4: 4, 'd' c7: 1, 'a'更底层的 MVCC 实现可进一步参考 src/storage/mvcc.rs 及其测试 src/storage/testscripts/mvcc(其中每个异常类型anomaly_*都有对应脚本),SQL 层写操作(插入/更新/删除与索引、外键、唯一约束的联动)可参考 src/sql/testscripts/writes 与 src/sql/testscripts/schema 目录下的脚本。
上手验证
如需在本地体验上述语法,可先按 README.md 启动一个五节点集群,再通过toysql客户端连接:
$ ./cluster/run.sh # 启动 5 节点(SQL 端口 9601-9605) $ cargo run --release --bin toysql # 连接 node 1(localhost:9601)随后即可逐条尝试本文中的建表、插入、查询、连接、聚合与事务语句,并用EXPLAIN观察执行计划。所有 SQL 功能的行为基准都固化在 src/sql/testscripts 下的 Goldenscript 测试脚本中,可对照阅读以加深理解。
【免费下载链接】toydb
Distributed SQL database in Rust, written as an educational project
相关推荐
3个关键步骤让老Mac装上新版macOS:OpenCore Legacy Patcher 上手指南
3个关键步骤让老Mac装上新版macOS:OpenCore Legacy Patcher 上手指南 目标只有一个:把苹果已经不再支持的新版 macOS,装到你那
操作系统固件驱动开发Zeek 脚本语言完全参考:类型系统、运算符、属性、声明语句与事件语义实战指南
Zeek 脚本语言完全参考:类型系统、运算符、属性、声明语句与事件语义实战指南 Zeek(原名 Bro)是一个强大的网络分析框架,与常见的 IDS 不同,它通过
网络安全网络IDSProof of SQL 支持语法全解析:数据类型、运算符与 SELECT 子句的可证明 SQL 规范
Proof of SQL 支持语法全解析:数据类型、运算符与 SELECT 子句的可证明 SQL 规范 本文是一份面向开发者的 PoSQL(Proof of S
区块链密码学数据库后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考