kevy4.0

RDS→kevy 建模 cookbook

你正在把一个关系型数据模型搬上 kevy。下面每个 recipe 用的都是已交付的原语——没有 roadmap 功能,没有「即将推出」。每一条都点名它替代的 RDS 概念,和承接它的 kevy 模式。

所有 recipe 背后的设计立场是:建模访问路径,而不是 schema。RDS 允许你把这个决定推迟给查询规划器;kevy 要你把它说出来——回报是服务时的微秒级页面(实测数字见 bench/VALIDATION-LEDGER.md)。

每个命令块都能对一台新起的本地 kevy 原样运行(kevy --port 6004;recipe 11–14、16 和 20 还需要 kevy.toml[feed] enabled = true——见 docs/cdc.md)。bench/cookbook_smoke.sh 会把(英文版)cookbook 里的每一行 kevy-cli 对一台一次性服务器执行一遍,保证这些命令块永远诚实。

1. 表与行

SQL 对应物CREATE TABLE + SELECT col FROM t WHERE id = ?——矩阵:表、行、列

一行就是一个带类型前缀的 hash:

kevy-cli -p 6004 HSET user:42 name ada email ada@example.com age 36
kevy-cli -p 6004 HGET user:42 name
kevy-cli -p 6004 HGET user:42 phone    # NULL = 字段缺席:天然回答 (nil)

2. 一对多、多对多

SQL 对应物:外键列 + 关联表;SELECT … FROM orders WHERE user_id = ?——矩阵:JOIN

关系由链接 key 承载,每一侧一个 set:

kevy-cli -p 6004 HSET order:1001 user_id 42 total 1999 status shipped
kevy-cli -p 6004 HSET order:1002 user_id 42 total 550 status pending
kevy-cli -p 6004 SADD user:42:orders 1001 1002       # 1-N:成员 = 订单 id
kevy-cli -p 6004 RPUSH order:1001:items sku-7 sku-9
kevy-cli -p 6004 SADD tag:urgent:orders 1001         # N-M:每侧一个 set
kevy-cli -p 6004 SADD order:1001:tags urgent

或者干脆跳过链接 key:把外键放进行里(上面的 user_id),声明一个索引——IDX.QUERY … EQ 42 就是这个世界的 SELECT … WHERE user_id = 42,一跳完成 hydrate:

kevy-cli -p 6004 IDX.CREATE order_user ON PREFIX order: FIELD user_id TYPE i64 KIND range
kevy-cli -p 6004 IDX.QUERY order_user EQ 42 FIELDS total status

3. 序列

SQL 对应物AUTO_INCREMENT / CREATE SEQUENCE + nextval()——矩阵:PRIMARY KEY、UNIQUE、AUTO_INCREMENT

kevy-cli -p 6004 INCR seq:order          # 取一个 id
kevy-cli -p 6004 INCRBY seq:order 100    # 块分配:一次发 100 个 id
                                         # 在应用内存里派发,用完再取

块分配是高吞吐形态;崩溃留缝的契约与 PostgreSQL 序列一致。

4. 乐观锁(行版本)

SQL 对应物UPDATE t SET …, version = v+1 WHERE id = ? AND version = v(版本列 CAS)——矩阵:事务

服务器侧:WATCH/MULTI——CAS 循环。事务是连接作用域的,所以要在一个 REPL 会话里跑(这里用 heredoc 喂入):

kevy-cli -p 6004 HSET user:42 balance 100 version 7
kevy-cli -p 6004 <<'TXN'
WATCH user:42
HGET user:42 version
MULTI
HSET user:42 balance 90 version 8
EXEC
TXN

WATCH 之后有人动过 user:42 的话,EXEC 回答 nil——竞争输了;重读重试。

Embedded 侧:把「读—判断—写」放进一个 atomic() 块——shard 锁让这个分支天然无竞争,不需要重试循环。

5. CHECK 约束与多 key 不变量

SQL 对应物CHECK (balance >= 0) + 触发器维护的审计行——矩阵:约束与触发器

RDS 在引擎里跑 CHECK (balance >= 0)。kevy 的替代是原子块内的读:应用评估不变量,引擎保证判断与写提交在一起。

// embedded——不许透支的扣款,附带一条审计行:
store.atomic(b"acct:7", |ctx| {
    let bal: i64 = parse(ctx.hget(b"acct:7", b"balance")?);
    if bal < amount { return Err(Overdraw); }
    ctx.hset(b"acct:7", &[(b"balance", &(bal - amount))])?;
    ctx.rpush(b"acct:7:ledger", &[entry])?;
    Ok(())
})

跨 shard 不变量:atomic_all_shards(确定性锁序,文档化的死锁豁免)。慎用——它是可串行化事务的大锤,而多数不变量按设计就住在一个 key 前缀底下。

6. 幂等键

SQL 对应物UNIQUE INDEX + INSERT … ON CONFLICT DO NOTHING——矩阵:PRIMARY KEY、UNIQUE、AUTO_INCREMENT

kevy-cli -p 6004 HSET req:9001 idem_key pay-2026-07-04-a77 amount 1999
kevy-cli -p 6004 IDX.CREATE req_idem ON PREFIX req: FIELD idem_key TYPE str KIND unique
kevy-cli -p 6004 IDX.QUERY req_idem EQ pay-2026-07-04-a77   # 重复以多命中读的形式可见
kevy-cli -p 6004 IDX.VERIFY req_idem                        # ……并在这里被计数
kevy-cli -p 6004 SET idem:pay-2026-07-04-a77 1 NX PX 86400000

先写行、再查询——重复是*可见的*(unique kind 在 VERIFY 里计数而不是拒绝写入;这是声明式栅栏,不是写闸门)。要硬闸门,就在处理之前用 SET … NX PX 形态:NX 是原子认领,TTL 是保留窗口。

7. 软删除

SQL 对应物deleted 标记列 + 部分索引 / 视图 WHERE deleted = 0——矩阵:VIEW

打标记,不移除:

kevy-cli -p 6004 HSET user:42 deleted 0 age 36
kevy-cli -p 6004 HSET user:43 deleted 1 age 51
kevy-cli -p 6004 IDX.CREATE user_live ON PREFIX user: FIELD deleted TYPE i64 KIND range
kevy-cli -p 6004 IDX.QUERY user_live EQ 0 LIMIT 100    # 只要活的行

视图把过滤条件永久组合掉——调用方再也不用复述它:

kevy-cli -p 6004 IDX.CREATE user_age ON PREFIX user: FIELD age TYPE i64 KIND range
kevy-cli -p 6004 VIEW.CREATE live_users QUERY '(' AND user_live EQ 0 user_age RANGE 18 200 ')' ORDER BY user_age
kevy-cli -p 6004 VIEW.QUERY live_users LIMIT 10

8. 复合排序(ORDER BY a, b)

SQL 对应物:复合索引上的 ORDER BY a, b——矩阵:ORDER BY / LIMIT / OFFSET

写入时把复合序编码进一个被索引的 score 字段:有界整数 bscore = a * 1_000_000 + b,字典序复合用零填充的字符串字段——一个索引、一个 ORDER BY;写钩子像维护普通字段一样维护它:

kevy-cli -p 6004 HSET evt:1 ord '2026-07-04|000042'
kevy-cli -p 6004 HSET evt:2 ord '2026-07-04|000007'
kevy-cli -p 6004 HSET evt:3 ord '2026-07-05|000001'
kevy-cli -p 6004 IDX.CREATE evt_ord ON PREFIX evt: FIELD ord TYPE str KIND range
kevy-cli -p 6004 IDX.QUERY evt_ord RANGE '2026-07-04|000000' '2026-07-04|999999' LIMIT 100

9. JSONB

SQL 对应物:JSON/JSONB 列 + 生成列索引——矩阵:类型系统

拍平成 hash 字段:profile.city → 字段 profile.city。你保住了按字段读写、字段级 TTL(HEXPIRE)和可索引性——JSONB 给你的一切,除了 JSON-path 查询,那是永久出局的(查询引擎斜坡;见 designing-on-kevy.md 的 REFUSED 表)。

kevy-cli -p 6004 HSET user:7 profile.city tokyo profile.plan pro
kevy-cli -p 6004 HGET user:7 profile.city
kevy-cli -p 6004 HEXPIRE user:7 3600 FIELDS 1 profile.plan   # 字段级 TTL 在拍平后依然可用

没人索引的深嵌套 blob 可以留成一个序列化字段;某条路径一旦要紧,就把它提升成字段。

10. 级联删除 / 外键

SQL 对应物FOREIGN KEY … ON DELETE CASCADE——矩阵:约束与触发器

级联是应用模式,从来不是引擎魔法:

kevy-cli -p 6004 HSET order:1001 user_id 42
kevy-cli -p 6004 RPUSH order:1001:items sku-7 sku-9
kevy-cli -p 6004 SADD order:1001:tags urgent
kevy-cli delete-prefix -p 6004 --rate 5000 order:1001:   # 子行清空,父行还在

11. 你不需要的 outbox

SQL 对应物:事务性 outbox 表 + 中继 worker——矩阵:CDC

事务性 outbox 模式之所以存在,是因为 RDS 提交和消息总线发布无法原子化。在 kevy 里feed 就是 outbox:每笔已提交的写入本来就是一个位于 (generation, offset) 游标处的变更帧,at-least-once、可按前缀过滤(docs/cdc.md)。消费 FEED.READ;别再造第二本日志。

# 需要 kevy.toml 里 [feed] enabled = true(见 ../cdc.md)
kevy-cli -p 6004 HSET order:9001 status paid
kevy-cli -p 6004 FEED.SHARDS
kevy-cli -p 6004 FEED.TAIL 0                             # 新消费者的起始游标
kevy-cli -p 6004 FEED.READ 0 1 0 COUNT 10 PREFIX order:  # gen 1 = 新数据目录的第一个 generation

12. 审计历史

SQL 对应物:触发器维护的审计/历史表(或 binlog 考古)——矩阵:CDC

CDC 的保留窗口就是审计日志:帧按提交顺序携带已应用效果的 argv。按你欠合规的窗口给 feed backlog 定容量,用游标消费者导出到冷存储。要做时间点重建:恢复快照 + 重放到 (gen, offset) 恢复点(persistence.md)。

kevy-cli -p 6004 HSET acct:7 balance 100
kevy-cli -p 6004 HSET acct:7 balance 90
kevy-cli -p 6004 FEED.READ 0 1 0 COUNT 100 PREFIX acct:   # 谁在什么时候设了什么,按提交顺序

13. 回滚窗口(反向镜像)

SQL 对应物:切换期间反向复制回旧主库——迁移 playbook 阶段 5

切换期间,跑一个把 kevy 写入镜像回旧 RDS 的 CDC 消费者(FEED.READ → UPDATE 语句)。这样你的回滚方案是「把应用指回去」,不是「反向迁移数据」。信心固化后退役镜像;kevy-cli diff(按前缀摘要)就是信心仪表。

kevy-cli -p 6004 HSET user:42 name ada
kevy-cli -p 6004 FEED.READ 0 1 0 COUNT 10 PREFIX user:   # 镜像消费者的读循环
kevy-cli diff 127.0.0.1:6004 127.0.0.1:6004 user:        # 摘要一致:安全形态的自检
kevy-cli diff old-rds-mirror.internal:6379 127.0.0.1:6004 user:   # needs-external

14. 分析导出

SQL 对应物:喂数仓的 ETL 作业 / binlog tap——矩阵:CDC

服务与分析不共用一个引擎。导出模式:

kevy-cli -p 6004 HSET order:1001 user_id 42 total 1999
kevy-cli export -p 6004 --prefix order: /tmp/orders.resp
kevy-cli -p 6004 FEED.READ 0 1 0 COUNT 100 PREFIX order:   # CDC 到数仓的读循环

15. 载入顺序(延迟索引规则)

SQL 对应物:先 LOAD DATA、后 CREATE INDEX(批量载入纪律)——矩阵:二级索引 DDL

批量载入,声明索引/视图:回填以约 7s/百万行的速度从既有行构建——比给每条导入行付写钩子便宜几个数量级(migration.md)。

kevy-cli -p 6004 HSET item:1 price 10
kevy-cli -p 6004 HSET item:2 price 25
kevy-cli -p 6004 HSET item:3 price 7
kevy-cli export -p 6004 --prefix item: /tmp/items.resp
kevy-cli import -p 6004 /tmp/items.resp   # 先批量载入:不付索引写钩子
kevy-cli -p 6004 IDX.CREATE item_price ON PREFIX item: FIELD price TYPE i64 KIND range   # 后声明:走回填
kevy-cli -p 6004 IDX.QUERY item_price RANGE 0 100 LIMIT 10

后面三个 recipe 换了负载:不再是被替换的 RDS,而是 AI agent 的记忆栈。不需要任何新东西——会话状态、情景记忆和 RAG 检索,就是同一批访问路径模式换了 key 前缀。

16. 带 TTL 的会话上下文

SQL 对应物:sessions 表 + 过期 cron——矩阵:容量估算与运维差异

agent 的工作上下文是一行带租约的数据:压缩后的对话住在 hash 里,EXPIRE 是闲置逐出策略(每轮续租——滑动窗口),feed 是审计轨迹——有人问「agent 在第 7 轮知道什么」时拿来重放。

# 需要 kevy.toml 里 [feed] enabled = true(见 ../cdc.md)
kevy-cli -p 6004 HSET session:a7 user 42 turns 6 messages 'wants refund for order 1001; tone calm' last_tool order_lookup
kevy-cli -p 6004 EXPIRE session:a7 3600
kevy-cli -p 6004 HSET session:a7 turns 7 messages 'refund approved; awaiting confirmation'
kevy-cli -p 6004 EXPIRE session:a7 3600                       # 每一轮都续租
kevy-cli -p 6004 FEED.TAIL 0                                  # 审计游标:日志现在的尾部
kevy-cli -p 6004 FEED.READ 0 1 0 COUNT 100 PREFIX session:    # gen 1 = 新数据目录的第一个 generation

messages 字段装的是你的压缩步骤产出的任何摘要;重写它就是一条 HSET,而每次修订本来就是提交序里的一个变更帧——多数 agent 框架外挂的「对话历史」表,就是 recipe 12 的审计日志白送给你。

17. 情景记忆(时间 × 语义)

SQL 对应物WHERE ts BETWEEN … + pgvector ORDER BY embedding <=> ? LIMIT k——矩阵:SELECT

情景记忆对同一批行回答两个问题:最近发生了什么(时间)和*什么与此相似*(语义)。一个前缀,每个问题一个索引——DIM 8 是为了演示可读;真实 embedding 是 768+ 维、以 f32-LE blob 传输,下面的 csv: 调试形态在任何接受向量的地方都可用(存储字段与查询向量走同一个解析器——vector-search.md)。

kevy-cli -p 6004 HSET mem:1 ts 1783200000 kind obs what 'user prefers dark roast' v csv:0.9,0.1,0,0,0,0,0,0
kevy-cli -p 6004 HSET mem:2 ts 1783203600 kind obs what 'user asked about decaf' v csv:0.8,0.3,0.1,0,0,0,0,0
kevy-cli -p 6004 HSET mem:3 ts 1783207200 kind reflection what 'coffee questions cluster in the morning' v csv:0,0.2,0.9,0.1,0,0,0,0
kevy-cli -p 6004 IDX.CREATE mem_ts ON PREFIX mem: FIELD ts TYPE i64 KIND range
kevy-cli -p 6004 IDX.CREATE mem_kind ON PREFIX mem: FIELD kind TYPE str KIND range
kevy-cli -p 6004 IDX.CREATE mem_ann ON PREFIX mem: FIELD v TYPE vector KIND ann DIM 8
kevy-cli -p 6004 IDX.QUERY mem_ts RANGE 1783203000 1783210000 LIMIT 10 FIELDS what      # 最近的记忆
kevy-cli -p 6004 IDX.QUERY mem_ann KNN csv:0.85,0.2,0,0,0,0,0,0 LIMIT 2 FIELDS what ts  # 相似的记忆
kevy-cli -p 6004 IDX.QUERY COMPOSE AND mem_ts RANGE 1783203000 1783210000 mem_kind EQ reflection LIMIT 10 FIELDS what

COMPOSE AND 合取标量腿(RANGE/EQ)——这里是「在这个时间窗内 AND 是一条 reflection」。至于*窗口内的相似*,有意不提供 KNN 腿(在图遍历里做过滤是查询引擎斜坡,REFUSED):给 LIMIT 留余量跑 KNN,像上面那样用 FIELDS hydrate ts,窗口外的命中在客户端丢弃。

18. 带混合检索的 RAG 分块

SQL 对应物:tsvector 全文 + pgvector KNN,应用侧融合——矩阵:SELECT

chunk 是同时携带两个检索面的行——文本和它的 embedding——一次写同时维护两个索引:

kevy-cli -p 6004 HSET chunk:1 doc kevy-guide seq 1 body 'rows are hashes under a typed key prefix' v csv:1,0,0,0,0,0,0,0
kevy-cli -p 6004 HSET chunk:2 doc kevy-guide seq 2 body 'indexes are declared once and maintained by the write hook' v csv:0,1,0,0,0,0,0,0
kevy-cli -p 6004 HSET chunk:3 doc kevy-guide seq 3 body 'the feed streams every committed write as a change frame' v csv:0,0,1,0,0,0,0,0
kevy-cli -p 6004 IDX.CREATE chunk_text ON PREFIX chunk: FIELD body TYPE str KIND text
kevy-cli -p 6004 IDX.CREATE chunk_ann ON PREFIX chunk: FIELD v TYPE vector KIND ann DIM 8
kevy-cli -p 6004 IDX.QUERY HYBRID chunk_text MATCH 'typed key prefix' chunk_ann KNN csv:0.9,0.1,0.1,0,0,0,0,0 LIMIT 2 FIELDS body
kevy-cli -p 6004 IDX.QUERY HYBRID chunk_text MATCH 'change frame' chunk_ann KNN csv:0,0.1,0.9,0,0,0,0,0 LIMIT 2 RRFK 20 FIELDS body

HYBRID 在服务端跑两条腿并以倒数排名融合(RRF)合并:每个 key 在 BM25 列表与 KNN 列表上各取 Σ 1/(k + rank)——只看排名,所以两种异质分数尺度永远不需要归一化;同时在*两条*腿都靠前的 chunk,胜过只霸一条腿的 chunk。RRFK 就是那个 k(默认 60):信得过每条腿的头部命中、想让「两边都同意」占主导时调低;想把融合拉平、让共识深入两个列表时调高。


最后两个 recipe 彻底离开机架:边缘节点上的 kevy——同一个服务器二进制,或裁到 core 档 655 KB 的 kevy-embeddediot.md)——说的还是同一套 verb,模式从数据中心到传感器网关逐字迁移。

19. 传感器缓存(最新值 + 存活租约)

SQL 对应物readings_latest upsert 表 + 陈旧度 cron——矩阵:容量估算与运维差异

每个传感器的当前值是一行数据;TTL 是存活契约。停止上报的传感器会从缓存中过期消失——缺席本身就是掉线信号,不用写清扫任务:

kevy-cli -p 6004 HSET sensor:t1 val 21.5 unit C ts 1783200000
kevy-cli -p 6004 EXPIRE sensor:t1 90
kevy-cli -p 6004 HSET sensor:t1 val 21.7 unit C ts 1783200030
kevy-cli -p 6004 EXPIRE sensor:t1 90      # 每次上报都续租
kevy-cli -p 6004 EXISTS sensor:t1         # 1 = 在报,0 = 已失联

租约按你的告警容忍度定(这里 90 s = 连丢三个 30 秒上报)。要*响应*传感器失联而不是轮询,就开启带 x(expired)类的 keyspace 通知并订阅过期事件——同一契约的推送形态(pubsub.md)。

最近窗口是一条带硬上限的 stream——MAXLEN ~ 让节点内存无论跑多久都有界,这在动辄数月不重启的边缘盒子上正是要紧的不变量:

kevy-cli -p 6004 XADD sensor:t1:log MAXLEN '~' 1000 '*' val 21.5
kevy-cli -p 6004 XADD sensor:t1:log MAXLEN '~' 1000 '*' val 21.7
kevy-cli -p 6004 XLEN sensor:t1:log
kevy-cli -p 6004 XRANGE sensor:t1:log - + COUNT 10

Embedded 形态:同一批 verb 走网关进程内的类型化 API——store.hset(…) / store.expire(…) / store.xadd(…)——完全不经过 socket;core feature 档承载本 recipe 用到的一切(iot.md)。

20. 边缘聚合(写时 GROUP BY + 上行)

SQL 对应物:每次仪表盘刷新重跑一遍的 SELECT zone, COUNT(*), SUM(w) … GROUP BY zone——矩阵:GROUP BY 与聚合

边缘节点就地汇总、只上传摘要——原始读数多到没法上行。聚合声明一次,由写路径维护,于是「聚合作业」直接不复存在:

kevy-cli -p 6004 HSET reading:1 zone floor1 w 120
kevy-cli -p 6004 HSET reading:2 zone floor1 w 180
kevy-cli -p 6004 HSET reading:3 zone floor2 w 95
kevy-cli -p 6004 IDX.CREATE zone_w ON PREFIX reading: FIELD w TYPE i64 KIND agg GROUPBY zone
kevy-cli -p 6004 IDX.QUERY zone_w GROUP floor1            # [count, sum, min, max, avg]
kevy-cli -p 6004 IDX.QUERY zone_w GROUPS BY sum LIMIT 10  # 按负载排名的 zone

上行就是 recipe 11 的 outbox 穿上工装:feed 本来就按提交顺序记着每笔写入,所以云同步消费者就是一个游标循环,链路断几个小时也能续——at-least-once、按提交序、按前缀过滤出云端要的那部分:

# 需要 kevy.toml 里 [feed] enabled = true(见 ../cdc.md)
kevy-cli -p 6004 FEED.TAIL 0
kevy-cli -p 6004 FEED.READ 0 1 0 COUNT 100 PREFIX reading:   # 上行循环

再配上 recipe 19 的 MAXLEN 上限和 TTL:原始读数在节点上有界,聚合行始终很小,feed 游标跨重启存活——除了 kevy 本身,整个边缘故事零活动件。

Recipe 索引

Recipe ↔ 它替代的 SQL 构造 ↔ 陈述语义与边界的 rds-workloads.md 矩阵行。

#RecipeSQL 构造矩阵行
1表与行CREATE TABLE、点查 SELECT表、行、列
2一对多、多对多外键列、关联表、WHERE fk = ?JOIN
3序列AUTO_INCREMENT / nextval()PK、UNIQUE、AUTO_INCREMENT
4乐观锁版本列 CAS UPDATE事务
5CHECK 约束CHECK (…) + 审计触发器约束与触发器
6幂等键UNIQUE INDEX + ON CONFLICT DO NOTHINGPK、UNIQUE、AUTO_INCREMENT
7软删除标记列 + 过滤视图VIEW
8复合排序ORDER BY a, bORDER BY / LIMIT / OFFSET
9JSONBJSON 列 + 生成列索引类型系统
10级联删除 / 外键ON DELETE CASCADE约束与触发器
11你不需要的 outbox事务性 outbox 表CDC
12审计历史审计表 / binlog 考古CDC
13回滚窗口切换期反向复制迁移 playbook
14分析导出ETL / binlog tap 到数仓CDC
15载入顺序先批量 LOAD DATA、后建索引二级索引 DDL
16带 TTL 的会话上下文sessions 表 + 过期 cron容量估算与运维差异
17情景记忆时间 BETWEEN + pgvector KNNSELECT
18RAG 混合检索tsvector + pgvector,融合SELECT
19传感器缓存upsert 表 + 陈旧度 cron容量估算与运维差异
20边缘聚合每刷新一遍 GROUP BY + ETL 上行GROUP BY 与聚合