“这个客户名下所有还没关闭的订单。”“这个购物车里有几件东西。”这类读,一个应用一秒钟要做上千次,而在关系型数据库里,每一次都是一条背后跟着查询计划器的查询。kevy 可以直接把答案备好。
键值存储被挡在应用数据之外,通常只因为一句反对:可是我需要按 key 以外的东西去查。这句反对是对的,而二级索引正是为它准备的。
索引是声明出来的,不是建出来的。你写清楚 key 的前缀和字段,写路径会把它维持在最新。一个带过滤的列表于是重新变回一次查表——没有计划器,没有扫描,没有查询。
视图更进一步,在写入时就把聚合维持在最新,于是一个计数、一个合计是读出来的,不是算出来的。这正是大多数应用真正在向 ORM 索要的东西,也正是它们的数据库忙成那样的原因。
而且整张表可以一次声明。TABLE.DECLARE 接受类型化列、二级索引和复合 ORDER BY 路径,在声明期把它们编译成具名索引——kevy-sql 用你手上现成的 PG/MySQL schema 文件做同一件事。引擎仍然不做任何规划、不强加任何 schema;join 和运行期 SQL 依旧按名拒绝。
“客户 881 的所有订单”始终是一次查表:每个要查的字段声明一个索引,照常写入,按值来读。
HSET order:1001 customer 881 status open total 4400
HSET order:1002 customer 881 status paid total 8400
HSET order:1003 customer 902 status open total 1200IDX.CREATE idx:cust ON PREFIX order: FIELD customer TYPE i64 KIND range
IDX.CREATE idx:status ON PREFIX order: FIELD status TYPE str KIND rangeIDX.QUERY idx:cust EQ 881
-> 1) "0" # cursor
2) 1) "order:1001" 2) "881"
3) "order:1002" 4) "881"IDX.QUERY COMPOSE AND idx:cust EQ 881 idx:status EQ open
-> 1) "0"
2) 1) 1) "order:1001"成本与限制 索引的账是每次写的时候付的,不是读的时候——对读多的服务路径这是对的交易,对写多的日志是错的。这里没有 join,以后也不会有:索引回答的是“哪些 key 匹配这些字段”,不是“把这两个集合连起来”。如果你的读确实需要 join,就把它留在 Postgres 里——关系型负载那一页写着哪些读属于这种情况。
一个带过滤、带排序的列表,由写路径一直维持着——读永远不用重算它,因为它从来没有过期过。
VIEW.CREATE v:open881 QUERY ( AND idx:cust EQ 881 idx:status EQ open ) ORDER BY idx:cust
-> OK括号是各自独立的参数。
VIEW.QUERY v:open881
-> 1) "0"
2) 1) "order:1001" 2) "881"成本与限制 视图是写路径上一直要干的活:每一次对匹配 key 的写都会更新它,不管今天有没有人来读。只为应用真正在对外服务的那些读声明视图,不再服务的就删掉。视图要组合的那些索引,必须先存在。
一张关系型表的读路径——索引化的 WHERE、余下的过滤、ORDER BY、翻页、COUNT——由一条声明编译到具名索引上。或者直接由你现成的 schema 文件编译。
TABLE.DECLARE orders PREFIX order: PK id COLUMN id str COLUMN customer i64 COLUMN status str COLUMN total f64 INDEX status range VALUES total customer ORDERPATH by_customer ON customer THEN total DESC
-> OK行仍是前缀下的普通 hash——缺列就是 NULL;kevy-cli sql compile schema.sql 会从 CREATE TABLE / CREATE INDEX 生成这一行。
IDX.QUERY orders.status EQ open FILTER total RANGE 2000 inf LIMIT 20
-> 1) "0"
2) 1) "order:1001" 2) "open"
IDX.COUNT orders.status EQ open
-> (integer) 2IDX.QUERY orders.by_customer WHERE customer EQ 881 LIMIT 20 FIELDS status total一个复合索引,用关系型复合索引的方式回答它——每个客户的订单,从大到小,不需要重排。
成本与限制 没有运行期 SQL,也没有 join。服务端把 SELECT 当作未知命令拒绝;kevy-cli sql compile 在构建期把 PG/MySQL schema 文件变成上面这些声明,并按名拒绝 JOIN、子查询和 GROUP BY,同时指向替代它们的配方。唯一性是校验而非强制,约束是配方而非引擎检查。开着分层存储时,index-only 查询即使全部行都是冷的也只读 RAM——只有最后的 FIELDS 页会读冷行,每行一次。