)
SpacetimeDB SQL 参考实战指南订阅语言、查询与 DML 语法全解析v1.12.0【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB本文以 SpacetimeDB v1.12.0 官方 SQL 参考文档为主体结合仓库内crates/sql-parser、crates/query、crates/expr、crates/subscription、crates/core等模块的源码实现系统讲解 SpacetimeDB 的两套 SQL 语言——用于实时数据复制的订阅语言Subscriptions与用于查询和数据变更的查询/DML 语言。读完本文你将掌握订阅查询的完整语法与限制、六类语句SELECT/INSERT/DELETE/UPDATE/SET/SHOW的 EBNF 与实战写法、SATS 数据类型在 SQL 中的字面量表示以及官方推荐的索引与查询优化最佳实践。SpacetimeDB 支持两套 SQL 子集它们共用同一个基于 PostgreSQL 方言的解析器但服务于完全不同的场景查询语言通过 CLI 或 HTTP API 发起支持SELECT及INSERT、DELETE、UPDATE、SET、SHOW等完整语句订阅语言通过 SDK 或 WebSocket API 发起是严格意义上只读的查询语言专用于把数据库中的行子集实时、增量地复制到客户端。两套语言的入口在仓库中分别对应两个独立解析器订阅解析器parse_subscription与 查询解析器parse_sql它们共享同一套表达式、字面量与谓词解析逻辑crates/sql-parser/src/parser/mod.rs但各自施加不同的语法约束。订阅语言Subscriptions订阅语言的完整语法可以用一条 EBNF 概括SELECT projection FROM relation [ WHERE predicate ]订阅语言严格是一种查询语言它的唯一目的就是把数据库中的一部分行复制到客户端并在数据库发生变化时自动、实时地更新这些行。订阅没有手动更新这个视图的上下文因此INSERT、DELETE这类数据操纵语句一概不被支持——在 订阅解析器 中任何非SELECT语句都会直接返回SubscriptionUnsupported::Dml错误。注意由于订阅是实时求值的性能至关重要因此订阅语言在即席查询之上施加了额外限制下文会逐一标出。例如增量订阅在编译期就会强制检查连接列是否有索引见 crates/subscription/src/lib.rs 中的has_non_index_join不满足时直接报错Subscriptions require indexes on join columns。SELECT 子句SELECT ( * | table . * )SELECT子句决定了要订阅哪张表。由于订阅 API 本质上是复制 API一条查询只能从单张表返回行且必须返回整行不允许单独的列投影如SELECT name。当表唯一无歧义时*可以直接使用否则必须用表名限定。示例-- 订阅一张表的全部行 SELECT * FROM Inventory -- 用表名限定 * 投影 SELECT item.* from Inventory item -- 订阅所有订单总额超过 $1000 的客户 SELECT customer.* FROM Customers customer JOIN Orders o ON customer.id o.customer_id WHERE o.amount 1000 -- 非法必须返回 Customers 或 Orders 其中之一不能同时返回两者 SELECT * FROM Customers customer JOIN Orders o ON customer.id o.customer_id WHERE o.amount 1000从语法实现看订阅的投影在 crates/sql-parser/src/parser/mod.rs 中只接受Project::Star(..)两种形式*或table.*任何列投影都会在解析阶段被拒绝。FROM 子句与 JOINFROM table [ [AS] alias ] [ [INNER] JOIN table [ [AS] alias ] ON column column ]虽然只能订阅单张表的行但可以在FROM子句中通过JOIN引用两张表。JOIN会选取输入表的所有行组合而ON决定哪些组合被保留。订阅语言不支持超过两张表的 JOIN。使用约束ON子句中引用的任何列必须用表名或别名限定为了高效求值订阅要求连接的两列上都定义索引这是编译器强制的要求见前述has_non_index_join检查。示例-- 订阅所有库存不足 10 件的产品的订单。 -- 必须在 Orders 表的 product_id 列以及 Product 表的 id 列上建立索引。 SELECT o.* FROM Orders o JOIN Inventory product ON o.product_id product.id WHERE product.quantity 10 -- 订阅所有至少被购买过一次的产品 SELECT product.* FROM Orders o JOIN Inventory product ON o.product_id product.id -- 非法ON 中引用的列必须限定表名 SELECT product.* FROM Orders JOIN Inventory product ON product_id id值得说明的是订阅 JOIN 的索引强制与两张表上限并非解析器层面的简单拦截而是由订阅的增量维护架构决定的。订阅被编译为一段段可增量执行的计划片段crates/subscription/src/lib.rs 中的Fragments简单SELECT只需 1 组插入/删除计划而JOIN需要 4 组计划片段来维护视图的增量delta。运行时通过索引扫描IxScan与索引连接IxJoin增量计算视图变化因此连接列必须有索引且订阅表数量越多、增量维护开销越大——这正是官方限制两张表 JOIN 的根源。WHERE 子句predicate expr | predicate AND predicate | predicate OR predicate ; expr literal | column | expr op expr ; op | | | | | ! | ; literal INTEGER | STRING | HEX | TRUE | FALSE ;SELECT子句决定订阅哪张表WHERE子句则决定订阅哪些行。订阅的 WHERE 不支持算术表达式如price * 2 100只支持上述比较运算符与AND/OR逻辑组合。从源码看运算符集合对应 crates/sql-parser/src/ast/mod.rs 中的BinOp,,,,,与LogOpAND,OR任何其他二元运算符都会被SqlUnsupported::BinOp拒绝。示例-- 找出售价超过 $X 的产品 SELECT * FROM Inventory WHERE price {X} -- 找出售价超过 $X 且库存少于 Y 件产品的产品 SELECT * FROM Inventory WHERE price {X} AND amount {Y}查询与 DML 语言查询语言是订阅语言的严格超集strict superset。主要差异体现在列投影与 JOIN 上订阅 API 只支持*投影而查询 API 支持单独的列投影以及COUNT聚合订阅 API 限制可连接的表数量并强制连接列索引约束而查询语言没有这些约束或限制。查询语言支持六类语句SELECTINSERTDELETEUPDATESETSHOW它们在 AST 层面对应 crates/sql-parser/src/ast/sql.rs 中的SqlAst枚举Select、Insert、Update、Delete、Set、Show。另外所有语句在解析后都会经过qualify_vars为单表查询自动限定列名与find_unqualified_vars在多表/JOIN 场景拒绝未限定列名两道规范化检查若一次提交多个语句或提交空语句则分别报MultiStatement与Empty错误。SELECTSELECT projection FROM relation [ WHERE predicate ] [LIMIT NUM]SELECT 子句projection * | table . * | projExpr { , projExpr } | aggExpr ; projExpr column [ [ AS ] alias ] ; aggExpr COUNT ( * ) [AS] alias ;SELECT子句决定返回哪些列。示例-- 查询库存中的全部条目 SELECT * FROM Inventory; -- 查询库存条目的名称与价格 SELECT item_name, price FROM Inventory查询语言还允许通过COUNT函数统计输入行数。COUNT永远返回一行结果即使输入为空。示例-- 统计库存条目的数量 SELECT COUNT(*) AS n FROM Inventory两个实现细节值得留意均有源码佐证聚合必须带有列别名SELECT COUNT(*) FROM t会被拒绝错误AggregateWithoutAlias参见 crates/sql-parser/src/parser/mod.rs 的parse_agg_fn查询语言还支持:sender参数占位符它代表发起请求的调用者身份Identity。解析阶段它被建模为Parameter::Sendercrates/sql-parser/src/ast/mod.rs随后会被解析器替换为对应的十六进制字面量resolve_sender例如DELETE FROM t WHERE owner :sender在 查询解析器测试 中即为合法语句。FROM 子句FROM table [ [AS] alias ] { [INNER] JOIN table [ [AS] alias ] ON predicate }与订阅不同查询 API支持连接两张以上的表。示例-- 找出所有订购了某产品及其订购时间的客户 SELECT customer.first_name, customer.last_name, o.date FROM Customers customer JOIN Orders o ON customer.id o.customer_id JOIN Inventory product ON o.product_id product.id WHERE product.name {product_name}WHERE 子句与订阅的 WHERE 子句 相同支持AND/OR组合与六种比较运算符同样不支持算术表达式。LIMIT 子句LIMIT通过指定上界来限制查询返回的行数。若查询本身返回的行更少LIMIT不会强行补足LIMIT不会对输入做任何排序或变换。示例-- 从库存中取出一行示例数据 SELECT * FROM Inventory LIMIT 1从解析实现看LIMIT只接受整数常量Value::NumberLIMIT b这类表达式形式会被SqlUnsupported::Limit拒绝同时查询本身不允许ORDER BY、GROUP BY、DISTINCT等标准 SQL 子句这使 SpacetimeDB 的 SQL 保持了一个小而精的受控子集详见 crates/sql-parser/src/parser/sql.rs 的parse_query/parse_select模式匹配。INSERTINSERT INTO table [ ( column { , column } ) ] VALUES ( literal { , literal } )示例-- 插入一行 INSERT INTO Inventory (item_id, item_name) VALUES (1, health1); -- 插入两行 INSERT INTO Inventory (item_id, item_name) VALUES (1, health1), (2, health2);从 crates/sql-parser/src/parser/sql.rs 的实现看INSERT只接受VALUES字面量列表parse_values每个值都必须是字面量不允许子查询每行的字面量个数与列数需在后续类型检查阶段保持一致。DELETEDELETE FROM table [ WHERE predicate ]DELETE删除一张表中的所有行若指定了WHERE则只删除匹配的行。DELETE不支持 JOIN。示例-- 删除全部行 DELETE FROM Inventory; -- 删除指定 item_id 的所有行 DELETE FROM Inventory WHERE item_id 1;实现层面多表 DELETEDELETE FROM t USING s ...等与带 JOIN 的 DELETE 都会被拒绝错误MultiTableDelete、DeleteTable且DELETE FROM的 FROM 列表中只允许单张表。UPDATEUPDATE table SET [ ( assignment { , assignment } ) ] [ WHERE predicate ]UPDATE更新一张表中已有行的列值。列由assignment形如column literal标识所有匹配WHERE条件的行都会被更新。更新发生在WHERE条件对所有行求值之后——即更新不会影响同一语句中WHERE的判定结果。UPDATE不支持 JOIN。示例-- 将指定 item_id 的所有行的 item_name 更新为新值 UPDATE Inventory SET item_name new name WHERE item_id 1;与 DELETE 一致带 JOIN 的 UPDATEUPDATE t JOIN s ...或UPDATE t SET ... FROM s在 查询解析器测试 中均被明确标记为不支持。SET⚠️警告SET语句是实验性的不保证与未来版本的 SpacetimeDB 兼容。SET var ( TO | ) literalSET更新一个系统变量的值。示例-- 拒绝扫描超过 1 万行的查询 SET row_limit 10000从源码看SET的概念只存在于 AST 中类型检查阶段会被重写为对st_var系统表的 INSERTcrates/expr/src/statement.rs 的type_and_rewrite_set-- SET var TO ... 被重写为 INSERT INTO st_var (name, value) VALUES (var, ...)SHOW⚠️警告SHOW语句是实验性的不保证与未来版本的 SpacetimeDB 兼容。SHOW varSHOW返回一个系统变量的值。与SET对称SHOW在类型检查阶段被重写为对st_var系统表的查询type_and_rewrite_show-- SHOW var 被重写为 SELECT value FROM st_var WHERE name var系统变量⚠️警告系统变量是实验性的不保证与未来版本的 SpacetimeDB 兼容。目前定义的系统变量为row_limit-- 拒绝扫描超过 1 万行的查询 SET row_limit 10000row_limit是当前唯一合法的系统变量名且值类型必须是 64 位无符号整数U64。从实现细节看变量名校验在 crates/expr/src/statement.rs 的is_var_valid中完成非法变量名报InvalidVar非数值字面量布尔、字符串、十六进制报类型不匹配变量的存储位于系统表st_varcrates/datastore/src/system_tables.rs其模式注释给出了示例值row_limit | (U64 5)运行时通过 crates/engine/src/relational_db.rs 的row_limit(tx)从st_var读取当前值当调用者不具备ExceedRowLimit权限crates/lib/src/identity.rs时查询执行前会先做基数估算若估算扫描行数超过row_limit查询在真正执行前即被拒绝crates/core/src/estimation.rs 的check_row_limit报错形如Estimated cardinality (N rows) exceeds limit (M rows)。这一点在 crates/core/src/sql/execute.rs 的test_row_limit集成测试中有完整演示向表T写入 5 行SET row_limit 4后普通客户端执行SELECT * FROM T失败而拥有内部权限的服务端仍可执行SET row_limit 5后外部查询恢复通过。需要说明的是row_limit作用于即席查询与订阅的新增查询权限系统区分了内部/外部调用方实际生产行为请以当前版本源码为准。此外查询入口还有一个全局硬性保护SQL 字符串长度上限为 50,000 字节crates/query/src/lib.rs 的MAX_SQL_LENGTH超长查询会被直接拒绝用于防止深度嵌套的AND/OR条件在编译时引发栈溢出。数据类型SpacetimeDB 支持的数据类型集合由SATSSpacetime Algebraic Type System时空代数类型系统定义。不过Spacetime SQL 并不支持 SATS 的全部能力尤其是不支持积类型product types与和类型sum types语言本身没有构造它们的途径也没有针对它们的标量运算符尽管如此包含这些类型的行仍然可以被查询返回给客户端例如读取包含结构体、枚举或Option类型列的表。字面量literal INTEGER | FLOAT | STRING | HEX | TRUE | FALSE ;下面说明如何在 Spacetime SQL 中为 SATS 数据类型构造字面量。布尔值布尔值使用规范的原子记号true或false表示。整数INTEGER [ | - ] NUM | [ | - ] NUM E [ ] NUM ; NUM DIGIT { DIGIT } ; DIGIT 0..9 ;SATS 支持多种固定宽度的整数类型字面量的具体类型由上下文推断。例如与u8列比较时按u8处理与i64列比较时按i64处理。整数字面量还支持科学计数法写法。示例-- 所有售价超过 $1000 的产品 SELECT * FROM Inventory WHERE price 1000 SELECT * FROM Inventory WHERE price 1e3 SELECT * FROM Inventory WHERE price 1E3浮点数FLOAT [ | - ] [ NUM ] . NUM | [ | - ] [ NUM ] . NUM E [ | - ] NUM ;SATS 同时支持 32 位与 64 位浮点类型字面量的具体类型同样由上下文推断。浮点字面量必须包含小数点并支持可选指数。示例-- 所有温度大于 105.3 的测量值 SELECT * FROM Measurements WHERE temperature 105.3 SELECT * FROM Measurements WHERE temperature 1053e-1 SELECT * FROM Measurements WHERE temperature 1053E-1字符串STRING { | CHAR } ;CHAR定义为 UTF-8 编码的 Unicode 字符。字符串字面量用单引号包裹内部连续两个单引号表示一个转义的单引号字符。示例SELECT * FROM Customers WHERE first_name John十六进制HEX X { HEXIT } | 0 x { HEXIT } ; HEXIT DIGIT | a..f | A..F ;十六进制字面量可以表示Identity、ConnectionId或二进制类型具体类型最终由上下文推断。两种写法等价X...与0x...。示例SELECT * FROM Program WHERE hash_value 0xABCD1234标识符identifier LATIN { LATIN | DIGIT | _ } | { | CHAR } ; LATIN a..z | A..Z ;标识符是用于标识数据库对象如表、列的词法单元。Spacetime SQL 同时支持带引号与不带引号的标识符两者均区分大小写。当标识符与 SQL 保留关键字冲突或表名/列名包含非字母数字字符时应使用带引号的标识符。由于 SpacetimeDB 使用 PostgreSQL 兼容的解析器在 PostgreSQL 中保留的关键字在 Spacetime SQL 中同样自动保留关键字全集可参考 PostgreSQL 官方文档的 SQL Key Words 附录。示例-- ORDER 是 SQL 关键字因此需要加引号 SELECT * FROM Order -- 包含 $ 的表名同样需要加引号 SELECT * FROM Balance$这一行为在解析器实现中体现为使用sqlparser的PostgreSqlDialect方言进行词法与语法分析crates/sql-parser/src/parser/sql.rs、crates/sql-parser/src/parser/sub.rs。从源码结构看多段命名空间限定名如子模块表lib.library_table会被点号连接成单一目录名parse_parts因此s.t形式的表引用、ns.t.a形式的限定列引用在测试中均被显式支持。性能与扩展性最佳实践在设计 schema 或编写查询时参考以下最佳实践以获得最优性能添加主键和/或唯一约束把取值必然互异的列约束为唯一键或主键。如果查询规划器知道连接值是唯一的它可以进一步优化 JOIN。为过滤列建索引为频繁出现在WHERE子句中的列建立索引索引能减少查询引擎扫描的行数。为连接列建索引为经常用作连接键即JOIN的ON条件中的列建立索引。这同样能减少回答查询时必须扫描的行数而且对订阅更新的性能至关重要——以至于它是一条编译器强制的硬性要求见上文订阅的 FROM 子句。如果某列已经被约束为唯一键或主键则无需再显式建索引因为这些约束会自动为该列建立索引。优化连接顺序在FROM子句中把过滤条件最具选择性的表放在最前面以最小化中间结果集大小提升查询效率。综合示例以此前出现过的查询为例-- 找出所有订购了某产品及其订购时间的客户 SELECT customer.first_name, customer.last_name, o.date FROM Customers customer JOIN Orders o ON customer.id o.customer_id JOIN Inventory product ON o.product_id product.id WHERE product.name {product_name}为符合上述性能与扩展性最佳实践需要在Inventory.name上建立索引因为查询对它做过滤将Inventory.id与Customers.id定义为主键在Orders.product_id与Orders.customer_id上定义非唯一索引Inventory应出现在FROM子句最前面因为它是WHERE中唯一提到的表Orders其次因为它直接与Inventory连接Customers再次因为它直接与Orders连接。对应的模块定义示例如下C# 与 Rust 两种服务端语言C#[SpacetimeDB.Table(Name Inventory)] [SpacetimeDB.Index(Name product_name, BTree [name])] public partial struct Inventory { [SpacetimeDB.PrimaryKey] public long id; public string name; .. } [SpacetimeDB.Table(Name Customers)] public partial struct Customers { [SpacetimeDB.PrimaryKey] public long id; public string first_name; public string last_name; .. } [SpacetimeDB.Table(Name Orders)] public partial struct Orders { [SpacetimeDB.PrimaryKey] public long id; [SpacetimeDB.Unique] public long product_id; [SpacetimeDB.Unique] public long customer_id; .. }Rust#[table( name Inventory, index(name product_name, btree [name]), public )] struct Inventory { #[primary_key] id: u64, name: String, .. } #[table( name Customers, public )] struct Customers { #[primary_key] id: u64, first_name: String, last_name: String, .. } #[table( name Orders, public )] struct Orders { #[primary_key] id: u64, #[unique] product_id: u64, #[unique] customer_id: u64, .. }按优化后的连接顺序重写查询-- 找出所有订购了某产品及其订购时间的客户 SELECT c.first_name, c.last_name, o.date FROM Inventory product JOIN Orders o ON product.id o.product_id JOIN Customers c ON c.id o.customer_id WHERE product.name {product_name};提示上述#[table]、#[primary_key]、#[unique]、#[index]属性在仓库的 bindings-macro 与各 SDK 测试模块如 modules/sdk-test 系列中都有大量实际用例可参考。附录公共产生式以下是本文档通篇使用的公共产生式table identifier ; alias identifier ; var identifier ; column identifier | identifier . identifier ;小结SpacetimeDB 的 SQL 以小而精为设计哲学订阅语言是面向实时复制的只读查询子集牺牲了灵活性换取增量维护的极致性能两表 JOIN 上限、连接列索引强制查询语言则在其之上放开列投影、多表 JOIN 与 DML构成完整的即席查询与数据操作能力。理解两套语言各自的语法边界——尤其是订阅的索引硬性要求与row_limit的基数保护——是写出高性能、可扩展应用的先决条件。若要进一步阅读代码建议从 sql-parser 解析器 出发沿 类型检查与语句重写 → 查询编译与执行 → 订阅增量维护 这条链路逐层深入。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考