ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpacetimeDB 订阅 SQL 为什么只支持单表整行投影,应如何编写

SpacetimeDB 订阅 SQL 为什么只支持单表整行投影,应如何编写 SpacetimeDB 订阅 SQL 为什么只支持单表整行投影应如何编写【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB在 SpacetimeDB 客户端里写订阅时很多人会习惯性地写下SELECT name, price FROM Inventory这类列投影语句结果订阅被服务端拒绝。这篇文章解决一个具体任务理解订阅 SQL 为什么只允许*/table.*这种单表整行投影并据此写出合法的订阅查询——包括 JOIN 过滤、WHERE 条件、索引要求以及需要列投影或跨表派生行时的替代写法。前提是你已经有一个包含 public 表的 module客户端通过 SDKTypeScript、C#、Rust、Unreal或 WebSocket API 连接。为什么订阅 SQL 只允许单表整行投影SQL Reference 对订阅语言的定位很明确它“strictly a query language”唯一用途是复制数据库中的一个行集合并在数据库变化时自动实时更新这份副本。由此推出两条硬约束整行、单表。订阅 API 是纯粹的 replication API查询只能返回单一表的行且必须返回完整行Individual column projections are not allowed。因为服务端要逐行追踪“哪些行属于这个订阅”以便增量推送更新行必须与表的一一映射保持一致列子集没有对应的更新语义。实时求值带来额外限制。文档原文说明Because subscriptions are evaluated in realtime, performance is critical, and as a result, additional restrictions are applied over ad hoc queries。所以订阅语法比 ad hoc 查询语法更严。由此得到的 SELECT 文法只有两种形式SELECT ( * | table . * )同时订阅没有手动更新视图的上下文INSERT、DELETE这类 DML 在订阅语句中不受支持。订阅 SQL 的完整语法与合法形式订阅语句的整体文法是SELECT projection FROM relation [ WHERE predicate ]SELECT子句决定订阅哪张表WHERE决定哪些行。文档给出的合法/非法示例如下{X}、{Y}是需要替换成具体数值的占位符例如 1000、10-- 订阅整张表的所有行 SELECT * FROM Inventory -- 表名有歧义时用表名限定 * SELECT item.* FROM Inventory item -- 通过 JOIN 引用第二张表做过滤但只复制 Customers 的整行 SELECT customer.* FROM Customers customer JOIN Orders o ON customer.id o.customer_id WHERE o.amount 1000 -- INVALID: 只能返回 Customers 或 Orders 之一不能同时返回两张表的行 SELECT * FROM Customers customer JOIN Orders o ON customer.id o.customer_id WHERE o.amount 1000注意第三、四个例子的对比JOIN 允许引用两张表用于过滤但投影即实际复制的行只能来自其中一张表。这就是“单表整行”的含义——单表指的是结果行来源不是 FROM 子句只能出现一张表。JOIN 的三个硬性条件订阅中的 JOIN 与 ad hoc 查询不同SQL Reference 的 FROM 小节 列出三个必须满足的条件最多 JOIN 两张表Subscriptions do not support joins of more than two tables。ON 子句中的列必须用表名或别名限定-- INVALID: ON 中的列名必须限定表 SELECT product.* FROM Orders JOIN Inventory product ON product_id id两个 JOIN 列都必须有索引subscriptions require an index to be defined on both join columns。这是为了 JOIN 能被高效求值也是编译器强制的要求compiler-enforced requirement缺索引的订阅查询无法通过。主键和 unique 约束会自动为列建索引不必重复显式建索引。合法示例要求Orders.product_id与Inventory.id上有索引-- 订阅库存少于 10 件的商品对应的所有订单 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.idWHERE 能写什么WHERE 子句的文法来自 SQL Referencepredicate expr | predicate AND predicate | predicate OR predicate ; expr literal | column | expr op expr ; op | | | | | ! | ; literal INTEGER | STRING | HEX | TRUE | FALSE ;不支持算术表达式。合法示例-- 售价高于 $X 的商品{X} 替换为你的价格阈值 SELECT * FROM Inventory WHERE price {X} -- 组合条件 SELECT * FROM Inventory WHERE price {X} AND amount {Y}需要列投影或跨表派生行时怎么办如果确实只需要几列、需要COUNT或者需要一张表里本来不存在的组合行不要试图绕开订阅 SQL 的限制而应换到对应的工具列投影和聚合用 ad hoc 查询 API。SQL Reference 的 Query 小节 说明查询语言是订阅语言的严格超集通过 [cli] 或 [http] API 发起的查询支持单列投影、COUNT、多于两张表的 JOIN且没有订阅的索引强制约束。比如SELECT item_name, price FROM Inventory只能在查询 API 里执行。区分点在于用途查询是一次性取数订阅是持续复制并实时更新。跨表派生行用 View。Views 文档 指出 View 是只读的、可计算的“表”可以在 module 里定义 View 完成 JOIN、聚合等派生逻辑然后像普通表一样订阅SELECT * FROM my_player; SELECT * FROM players_for_level;被订阅的 View 在其底层表变化时会自动更新。View 定义见 Views 文档Rust 用#[spacetimedb::view]C# 用[SpacetimeDB.View]特性TypeScript 用spacetimedb.view/spacetimedb.anonymousView。用 SDK 下发订阅并验证结果各 SDK 都把类型安全的 query builder 作为推荐默认raw SQL 在需要直接控制语法时可用Subscriptions 文档 确认“Query builders are the recommended default across SDKs, with raw SQL available where needed”。Rust SDK 的subscribe直接接受 SQL 字符串单个字符串或字符串数组/切片例如按上文语法传入// 文档示例用 SQL 字符串下发订阅 let conn DbConnection::builder() .with_uri(wss://maincloud.spacetimedb.com) .with_database_name(my_module) .build(); conn.subscription_builder() .on_applied(|ctx| { println!(Subscription ready!); for user in ctx.db.user().iter() { println!(User: {}, user.name); } }) .subscribe(SELECT * FROM user);TypeScript SDK 的典型路径是 query buildertables由生成的module_bindings导出{...}之外的值均来自你的 module 表结构import { DbConnection, tables } from ./module_bindings; const conn DbConnection.builder() .withUri(wss://maincloud.spacetimedb.com) .withDatabaseName(my_module) .onConnect((ctx) { // 过滤查询等价于 SELECT * FROM user WHERE online true ctx.subscriptionBuilder() .onApplied(() { // 初始行已进入客户端缓存 for (const user of ctx.db.user.iter()) { console.log(User: ${user.name}); } }) .subscribe([ tables.user, tables.user.where(r r.online.eq(true)), ]); }) .build();结果验证依据来自 Subscriptions 文档 与 Subscription Semantics成功信号onApplied回调在“所有初始匹配行已进入本地缓存”时触发一次此时ctx.db中可迭代到初始行。多个订阅集的更新会被打包进同一条TransactionUpdate客户端缓存始终反映提交后的数据库状态。失败信号onError回调在订阅被拒绝或服务端异常终止时触发TypeScript 参考 明确指出它“most frequently caused by passing an invalid query tosubscribe”——即查询语法不合法多表投影、列投影、缺索引的 JOIN 等时最先看到的就是这个回调。生命周期订阅句柄的isActive表示匹配行当前在缓存中isEnded表示已因取消订阅或错误结束unsubscribe是异步操作行在取消操作被应用后才从缓存移除。限制清单写订阅 SQL 时可以对照以下边界自查全部来自 SQL Reference能力订阅 SQLad hoc 查询 SQL列投影 /COUNT不支持仅*或table.*支持JOIN 表数最多 2 张不限JOIN 列索引强制要求无强制要求WHERE 算术表达式不支持见查询语言文法INSERT/DELETE不支持支持另外两条容易踩的限制WHERE 里不能写算术表达式只能写字面量、列和比较/布尔运算符*投影在表名歧义时必须写成table.*。如果同一份数据需要“先全量后按需”的订阅管理Subscriptions 文档的最佳实践 还建议把生命周期相同的查询放进同一个订阅替换订阅时先订阅新集合再取消旧的SpacetimeDB 订阅是零拷贝的重复订阅同一查询不产生额外开销避免大量重叠的查询否则服务端会对同一行重复求值和序列化。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进