1. 总体结构
1.1 Key 符号
| 符号 | Key 中的实际含义 |
|---|---|
/ |
全局根 |
* |
ID 作用域、表或记录分隔 |
! |
Catalog 或内部状态类别 |
+ |
索引数据根 |
~ |
图邻接数据 |
& |
访问授权根,或字段反向引用 |
# |
Change Feed |
% |
Live Query Event |
\0 |
可变长字符串终止符;内部 0x00/0x01 会先转义 |
<u32be> |
固定 4 字节无符号大端整数 |
<u64be> |
固定 8 字节无符号大端整数 |
<i64lex> |
符号位翻转后按 8 字节大端编码,可保持有符号数顺序 |
<RIDK> |
RecordIdKey 的可排序编码 |
1.2 分层图
text
SurrealQL Schema / Data
|
+-- Catalog 定义 ---------------- revision Value
|
+-- Record / Graph / Reference --- revision Value 或空 Value
|
+-- Secondary / FullText / ANN --- 排序 Key + 多种专用 Value
|
v
typed set/get/scan/del
|
+-- Key: storekey 顺序保持编码
+-- Value: revision / raw / u64be / Roaring
|
v
统一逻辑 KV 字节
|
+-- Memory
+-- SurrealKV
+-- RocksDB
+-- TiKV
`-- IndexedDB
1.3 公共读写伪代码
text
write(typed_key, typed_value):
raw_key = storekey_encode(typed_key)
raw_value = typed_value.kv_encode_value()
backend.set(raw_key, raw_value)
read(typed_key):
raw_key = storekey_encode(typed_key)
raw_value = backend.get(raw_key)
context = typed_key.value_context()
return typed_value.kv_decode_value(raw_value, context)
scan(prefix):
return backend.scan(prefix + 0x00, prefix + 0xff)
记录读取中的 context 是从 Key 恢复的完整 RecordId;这使 Key 成为记录身份的最终事实来源。
2. 当前版本真实 KV 示例
以下五组字节均由当前生产 Key/Value 编码器生成,经 Memory 事务写入后再用原始 Key 读取,读取结果与编码结果逐字节相等。示例共同使用 NamespaceId=1、DatabaseId=2、表 person。
2.1 Namespace 定义
逻辑对象:NamespaceDefinition { id: 1, name: "app", comment: "demo" }
Key,8 字节
text
hex: 2f216e7361707000
分段: 2f 216e73 617070 00
/ !ns app 终止
Value,12 字节
text
hex: 010103617070010464656d6f
分段: 01 | 01 | 03 617070 | 01 | 04 64656d6f
定义修订1
namespace_id=1
name长度3 + "app"
comment=Some
长度4 + "demo"
Key/Value 关系: Key 以名称支持 Catalog 前缀扫描;Value 保存稳定数值 ID 和完整定义。名称到 ID 的绑定因此可通过一次 Key 点查得到。
2.2 记录 person:42
逻辑 Value:
text
{
active: true,
id: person:42,
name: "Ada",
score: -7
}
Key,29 字节
text
hex:
2f2a000000012a000000022a706572736f6e002a02800000000000002a
分段:
2f | 2a 00000001 | 2a 00000002 | 2a 706572736f6e 00
/ ns=1 db=2 table="person"
2a | 02 | 800000000000002a
* RIDK:Number i64(42) 的符号位翻转大端编码
Value,87 字节
text
hex:
0252000000080000000900000000024a43000000023e0000000004026964024e
0b0000000106706572736f6e010054046e616d65024404000000034164610573
636f726502430300000001000d06616374697665022201
主要分段:
text
02 Record 修订2
52 000000 Record 负载长度 82,小端
08 000000 09 000000 两个字段的偏移表
00 metadata=None
02 4a 43 000000 Value 修订2、Object 标签、Object 长度67
02 3e 000000 Object 修订2、Object 负载长度62
00 04 小集合线性格式、4个键值对
02 "id" 02 4e ... id -> RecordId(person:42)
04 "name" 02 44 ... name -> String("Ada")
05 "score" 02 43 ... score -> Number::Int(-7)
06 "active" 02 22 01 active -> Bool(true)
Key/Value 关系: Key 中的 person:42 是权威身份。Value 即使包含不同的顶层 id,解码后也会被 Key 重建的 RecordId 覆盖。一个记录的普通字段全部内联在同一个 Value 中。
2.3 非唯一复合索引
索引:IndexId=3,索引字段 ["Ada", -7],记录 person:42。
Key,49 字节
text
hex:
2f2a000000012a000000022a706572736f6e002b000000032a06416461000540
e9e87f010000000302800000000000002a
主要分段:
text
/*<ns=1>*<db=2>*person\0+<ix=3>*
06 "Ada" 00 Value::String 的可排序编码
05 40e9e87f 0100 00 Number(-7) 的统一十进制顺序编码
00 复合字段 Array 结束
03 RecordIdKey=Some
02 800000000000002a RIDK:Number(42)
Value,11 字节
text
hex: 0106706572736f6e010054
解码: RecordId(person:42)
Key/Value 关系: RecordIdKey 进入 Key,所以相同字段值可对应多条记录且仍保持 Key 唯一。Value 保存完整 RecordId,扫描命中后不需要从 Key 的表作用域与尾部再拼装返回标识。
2.4 唯一复合索引
索引:IndexId=4,索引字段仍为 ["Ada", -7]。
Key,40 字节
text
hex:
2f2a000000012a000000022a706572736f6e002b000000042a06416461000540
e9e87f0100000002
尾部 02 表示 RecordIdKey=None,因此同一字段组合只能形成一个 Key。
Value,11 字节
text
hex: 0106706572736f6e010054
解码: RecordId(person:42)
Key/Value 关系: 字段组合放在 Key,拥有者放在 Value;写入使用"仅当旧 Value 不存在"的条件写。若组合中含 NONE 或 NULL,实现会退化为带 RecordIdKey 的非唯一形式,从而允许多个空值。
2.5 COUNT 索引的压实基线
索引:IndexId=5,压实后计数 65535。
Key,37 字节
text
hex:
2f2a000000012a000000022a706572736f6e002b000000052169750203000000
000000ffff
分段:
/*<ns=1>*<db=2>*person\0+<ix=5>!iu
02 uid=None,表示压实基线
03 pos=true
000000000000ffff count=65535,大端
Value,0 字节
text
hex: <empty>
Key/Value 关系: 计数本身进入 Key,Value 为空。普通更新写入带节点/事件 UUID 的正负增量 Key;压实器汇总已观察增量、删除这些增量,再写一个无 UUID 的基线 Key。
3. Schema 到 KV 的完整映射
3.1 用户可见 Catalog
下表中的 <ns>、<db>、<ix> 均是 4 字节大端 ID;名字是零结尾、可转义字符串。
| Schema 对象 | Key | Value |
|---|---|---|
| 存储格式主版本 | !v |
2 字节大端 MajorVersion;当前最新值为 3 |
| Namespace | /!ns<name>\0 |
NamespaceDefinition,revision |
| Database | /*<ns>!db<name>\0 |
DatabaseDefinition,revision |
| Table | /*<ns>*<db>!tb<table>\0 |
TableDefinition,revision |
| Field | /*<ns>*<db>*<table>\0!fd<field-path>\0 |
FieldDefinition,revision |
| Event | /*<ns>*<db>*<table>\0!ev<name>\0 |
EventDefinition,revision |
| Index Definition | /*<ns>*<db>*<table>\0!ix<name>\0 |
IndexDefinition,revision |
| Index ID 反查名称 | /*<ns>*<db>*<table>\0!il<ix> |
原始 UTF-8 索引名 |
| View 反向依赖 | /*<ns>*<db>*<source>\0!ft<view>\0 |
视图表的 TableDefinition,revision |
| 表级 Live Query | /*<ns>*<db>*<table>\0!lq<uuid> |
SubscriptionDefinition,revision |
| Root User | /!us<name>\0 |
UserDefinition,revision |
| Namespace User | /*<ns>!us<name>\0 |
UserDefinition,revision |
| Database User | /*<ns>*<db>!us<name>\0 |
UserDefinition,revision |
| Root Access | /!ac<name>\0 |
AccessDefinition,revision |
| Namespace Access | /*<ns>!ac<name>\0 |
AccessDefinition,revision |
| Database Access | /*<ns>*<db>!ac<name>\0 |
AccessDefinition,revision |
| Root Access Grant | /&<access>\0!gr<grant>\0 |
AccessGrant,revision |
| Namespace Access Grant | /*<ns>&<access>\0!gr<grant>\0 |
AccessGrant,revision |
| Database Access Grant | /*<ns>*<db>&<access>\0!gr<grant>\0 |
AccessGrant,revision |
| API | /*<ns>*<db>!ap<name>\0 |
ApiDefinition,revision |
| Analyzer | /*<ns>*<db>!az<name>\0 |
AnalyzerDefinition,revision |
| Bucket | /*<ns>*<db>!bu<name>\0 |
BucketDefinition,revision |
| Database Config | /*<ns>*<db>!cg<type>\0 |
ConfigDefinition,revision |
| Root Config | /!cg<type>\0 |
ConfigDefinition,revision |
| Function | /*<ns>*<db>!fn<name>\0 |
FunctionDefinition,revision |
| Module | /*<ns>*<db>!md<name>\0 |
ModuleDefinition,revision |
| ML Model | /*<ns>*<db>!ml<name>\0<version>\0 |
MlModelDefinition,revision |
| Parameter | /*<ns>*<db>!pa<name>\0 |
ParamDefinition,revision |
| Sequence Definition | /*<ns>*<db>*sq<name>\0 |
SequenceDefinition,revision |
3.2 定义中内嵌、没有独立 Key 的 Schema
- TableDefinition Value 内嵌:SCHEMAFULL/SCHEMALESS、表类型、Relation、ViewDefinition、权限、Change Feed、GraphQL 别名和弃用信息。
- FieldDefinition Value 内嵌:字段 Kind、只读/灵活、默认值、VALUE、ASSERT、COMPUTED、按动作权限、REFERENCE 和别名。
- IndexDefinition Value 内嵌:索引列路径、
Idx/Uniq/FullText/Count/Hnsw/DiskAnn类型及参数、注释、并发构建状态标志。 - ApiDefinition Value 内嵌:API Action、Method、Config 和 Middleware。
- AccessDefinition Value 内嵌:Record/Bearer/JWT 类型、认证表达式、签发与校验参数。
因此当前版本:
- 没有独立主键定义 KV;RecordIdKey 就是记录身份。
- 没有独立 CHECK KV;约束表达式在 FieldDefinition Value 的 ASSERT 中。
- 没有独立外键 KV;Record 类型约束在 FieldDefinition Value,启用 REFERENCE 时另写反向引用数据。
- 没有独立 View SQL KV;视图定义在 TableDefinition Value,源表下只写反向依赖。
- 没有独立 API Action KV;Action 在 ApiDefinition Value 中。
- 没有表分区、分片放置或每记录 TTL 的公共 Schema Key。
3.3 逻辑类型 Kind 的完整清单
字段 Kind 本身存入 FieldDefinition Value,不生成独立 KV。
| Kind | Value 中的效果 |
|---|---|
| Any | 允许任意实际 Value |
| None | Value::None |
| Null | Value::Null |
| Bool | Value::Bool |
| Bytes | Value::Bytes |
| Datetime | Value::Datetime |
| Decimal | Value::Number::Decimal |
| Duration | Value::Duration |
| Float | Value::Number::Float |
| Int | Value::Number::Int |
| Number | 三种 Number 之一 |
| Object | Value::Object |
| String | Value::String |
| Uuid | Value::Uuid |
| Regex | Value::Regex |
| Table | Value::Table,可限制表集合 |
| Record | Value::RecordId,可限制目标表集合 |
| Geometry | Value::Geometry,可限制 Point、Line、Polygon、MultiPoint、MultiLine、MultiPolygon、Collection |
| Either | 实际 Value 为成员 Kind 之一 |
| Set | Value::Set,内嵌元素 Kind 和可选长度约束 |
| Array | Value::Array,内嵌元素 Kind 和可选长度约束 |
| Function | 对应 Closure;类型定义可持久化,但 Closure 运行值拒绝持久化 |
| Range | Value::Range |
| Literal | 限定 String、Integer、Float、Decimal、Duration、Array Kind、Object Kind 或 Bool |
| File | Value::File,可限制 Bucket 集合 |
3.4 ID 和用户序列
Namespace、Database、Table、Index 和用户 Sequence 使用"每节点状态 + 已分配批次"两类 KV,避免每生成一个 ID 都争用同一个 Key。
| 用途 | 状态 Key / Value | 批次 Key / Value |
|---|---|---|
| Namespace ID | /!ni<node-uuid> / SequenceState |
/!nh<i64lex-start> / BatchValue |
| Database ID | /*<ns>!di<node-uuid> / SequenceState |
/*<ns>!dh<i64lex-start> / BatchValue |
| Table ID | /*<ns>*<db>!ti<node-uuid> / SequenceState |
/*<ns>*<db>!th<i64lex-start> / BatchValue |
| Index ID | /*<ns>*<db>*<table>\0!is<node-uuid> / SequenceState |
同前缀 !ih<i64lex-start> / BatchValue |
| 用户 Sequence | /*<ns>*<db>!sq<name>\0!st<node-uuid> / SequenceState |
同前缀 !ba<i64lex-start> / BatchValue |
SequenceState 和 BatchValue 均是 revision Value。索引 ID 不复用,因此异步删除旧索引前缀不会误删同名新索引。
TableId 虽然被分配并保存在 TableDefinition Value 中,但普通记录、图、引用和索引 Key 仍使用 TableName;NamespaceId、DatabaseId 和 IndexId 才直接进入这些数据 Key。
3.5 Schema 写入、修改和删除
text
DEFINE:
分配稳定 ID
set(catalog_key, revision_definition_value)
必要时写 id -> name、view dependency、sequence state
ALTER:
get(catalog_key)
修改内存定义
set(catalog_key, 整个新 revision Value)
REMOVE TABLE:
删除 TableDefinition Key
删除 /*ns*db*table\0 前缀内的记录、字段、事件、索引和内部状态
REMOVE NAMESPACE / DATABASE / INDEX:
先删除可见定义
在根空间写 /!rc... -> ReclaimState
后台等待安全期后删除目标前缀
普通删除使用事务层逻辑删除;EXPUNGE 路径使用清除所有版本的操作。Reclaim Key 位于根空间,所以目标 Namespace、Database 或 Index 前缀被删后,回收任务仍然存在。
4. Data 到 KV 的完整映射
4.1 普通记录
Key
text
/*<ns>*<db>*<table>\0*<RIDK>
RecordIdKey 的完整变体为:
- Number:变体标签 + 符号位翻转的 8 字节大端 i64。
- String:变体标签 + 零结尾、可转义字符串。
- Uuid:变体标签 + 16 字节 UUID。
- Array:变体标签 + 递归元素编码 + 容器终止符。
- Object:变体标签 + 按可排序形式编码的字段和值 + 容器终止符。
- Range:变体标签 + 起止 Bound 及递归 RecordIdKey。
Value
text
Record {
metadata: Option<Metadata>,
data: Value
}
Record Value 当前使用修订 2 的优化结构格式:
- Value 枚举标签包含变体编号和 inline/fixed/varlen 大小类别。
- varlen Value 带 4 字节小端长度,可跳过未知或不需要的值。
- Object、Array、Set 少于 8 个元素时使用线性正文;达到 8 个元素时增加偏移表。
- 没有公共压缩头、校验和、列式 null bitmap 或大 Value 分块引用。
记录点查由完整 Key 完成;表扫描使用:
text
begin = /*<ns>*<db>*<table>\0*00
end = /*<ns>*<db>*<table>\0*ff
更新普通字段会重写整个 Record Value。嵌套对象和数组也在同一 Value 内,不按列拆分。
4.2 所有可持久化运行时 Value
| Value 变体 | Record Value 中的编码 |
|---|---|
| None、Null | 内联枚举标签 |
| Bool | 固定 1 字节负载 |
| Number | Int i64、Float f64、Decimal 的 revision 编码 |
| String | 长度 + UTF-8 |
| Duration | revision 数值负载 |
| Datetime | UTC 时间负载 |
| Uuid | 16 字节 UUID 负载 |
| Array | revision 容器,元素递归 Value |
| Set | revision 容器,元素递归 Value |
| Object | revision Map,键为 Strand、值递归 Value |
| Geometry | Point、Line、Polygon、MultiPoint、MultiLine、MultiPolygon、Collection |
| Bytes | 长度 + 原始字节 |
| Table | TableName |
| RecordId | TableName + RecordIdKey |
| File | Bucket/对象 Key 的引用 |
| Regex | revision 字符串形式 |
| Range | Bound + 递归 Value |
| Closure | 编码器明确拒绝,不能作为持久化 Record Value |
Regex 可以进入 Record Value,但不能进入使用 storekey 的排序 Key;Closure 在 revision Value 和 storekey Key 两条路径都拒绝编码。
File Value 只保存 Bucket 和对象 Key。文件正文通过 ObjectStore 接口保存,不作为主 KV 事务中的大 Value。
4.3 数值和容器在排序 Key 中的规则
- 无符号固定整数按大端写入,因此字节序等于数值序。
- 有符号固定整数先异或最小值,再按大端写入。
- 浮点顺序编码按符号变换,普通 Key 可保持全序。
- Option 使用
None=0x02、Some=0x03;Bool 使用false=0x02、true=0x03。 - 字符串和字节片中的
0x00/0x01以0x01前缀转义,再写0x00终止符。 - Array、Set、Object 递归编码;元素或字段结束后再写容器终止符。
- 普通 storekey 格式中的 Value::Number 同时保存规范数值和 NumberKind,因此保留 Int/Float/Decimal 原始种类;RecordIdKey::Number 本身只接受 i64。
- 索引专用 IndexFormat 只保留规范数值,忽略 NumberKind;数值相等的
0、0.0、0dec产生相同索引 Key 字节。 - IndexFormat 对负无穷、有限数、正无穷和 NaN 给出稳定全序位置。
4.4 图关系
图数据的 Key 根为:
text
/*<ns>*<db>*<owner-table>\0~<owner-RIDK>
一次 RELATE left -> edge -> right 在同一事务写四个空 Value 的邻接 Key:
text
edge --IN--> left
edge --OUT--> right
left --OUT--> edge --target--> right
right --IN--> edge --target--> left
Key: 包含方向、外部表、边 RecordIdKey;顶点侧的新格式还包含远端目标表与 RecordIdKey。
Value: (),即空字节。
Key/Value 关系: 遍历所需信息全部在 Key。顶点侧携带 target 后,查远端顶点不必先读取边记录;若查询边属性,仍需读取正常的 edge Record Key/Value。边记录本身是普通 Record Value,metadata 标记 Edge,并保存 in/out 字段。
读取器兼容没有 target 的旧图 Key;当前写入使用带 target 的版本,并在相关更新中清理旧式 Key。
4.5 字段 REFERENCE 反向引用
仅当字段定义启用 REFERENCE 时写:
text
Key:
/*<ns>*<db>*<target-table>\0&<target-RIDK>
<origin-table>\0<origin-field>\0<origin-RIDK>
Value:
<empty>
字段值可以是单个 RecordId、Array 或 Set。更新时实现对旧目标集合和新目标集合做差集,分别删除和新增反向引用 Key。
Key 的字段顺序支持以下前缀扫描:
- 目标记录的全部引用。
- 目标记录中来自某张源表的引用。
- 目标记录中来自某张源表某字段的引用。
未声明 REFERENCE 的普通 RecordId 只内联在 Record Value,不产生额外 KV。
4.6 变更流和实时事件
| 数据 | Key | Value |
|---|---|---|
| Change Feed | /*<ns>*<db>#<versionstamp-slice>\0*<table>\0 |
TableMutations,revision |
| Live Query Event | /*<ns>*<db>%<versionstamp-slice>\0*<table>\0 |
LiveEvents,revision |
Versionstamp 作为可转义字节片进入 Key,所以同一 Database 内按时间有序;# 和 % 将两类数据隔离。它们是独立历史/事件 KV,不是普通 Record Key 的后缀版本。
4.7 运行时管理 KV
| 用途 | Key | Value |
|---|---|---|
| 集群节点 | /!nd<uuid> |
Node,revision |
| 节点 Live Query 路由 | /$<node-uuid>!lq<lq-uuid><ns><db> |
NodeLiveQuery,revision |
| Task Lease | /!tl<task-type> |
TaskLease,revision |
| 异步 Event Queue | /!eq<ns><db><table><event><time><node> |
AsyncEventRecord,revision |
| Index Compaction Queue | /!ic<ns><db><table><ix><node><uuid> |
空 Value |
| Deferred Reclaim | /!rc<kind><ns><db><table><ix><expunge><uuid> |
ReclaimState,revision |
Reclaim kind 目前只覆盖 Namespace、Database、Index。Table 删除直接处理表定义和表前缀。
5. 索引到 KV 的完整映射
当前 IndexDefinition 的实现集合恰好是:
text
Idx | Uniq | FullText | Count | Hnsw | DiskAnn
没有第七种索引实现,也没有独立的 covering/include-column、descending 或 collation 字段。普通索引列是字段路径;COUNT 可带条件;数组值会按索引求值规则展开组合,显式 FLATTEN 改变展开行为。
专用参数也全部内嵌在 IndexDefinition Value:
- FullText:Analyzer 名称、是否 Highlight、
BM25(k1,b)或 VectorSearch 评分。 - HNSW:维度、Distance、VectorType、
m/m0/ml/ef_construction、候选扩展、保留裁剪连接、是否使用向量哈希。 - DiskANN:维度、Distance、VectorType、目标 degree、
l_build、alpha、是否使用向量哈希。 - Distance:Chebyshev、Cosine、Euclidean、Hamming、Jaccard、Manhattan、Minkowski、Pearson、CosineNormalized、InnerProduct。
- VectorType:F64、F32、F16、I64、I32、I16、I8、U8;相同集合也用于 SerializedVector Value。
5.1 Idx 非唯一索引
text
Key:
/*<ns>*<db>*<table>\0+<ix>*<indexed-values-array><Some RIDK>
Value:
RecordId,revision
- 复合列按定义顺序组成 Array。
- RecordIdKey 在 Key 尾部消除重复冲突。
- 范围查询直接扫描字段值前缀。
- 结果仍需回表读取 Record Value,除非执行只需要 RecordId。
5.2 Uniq 唯一索引
text
Key:
/*<ns>*<db>*<table>\0+<ix>*<indexed-values-array><None>
Value:
RecordId,revision
- 唯一性由条件写保证:Key 不存在才可写入。
- 删除使用条件删除,只有 Value 仍属于目标 RecordId 才删除。
- 任一索引列为 NONE/NULL 时改用带 RIDK 的非唯一 Key,允许多个空值。
5.3 COUNT 索引
text
增量 Key:
/*<ns>*<db>*<table>\0+<ix>!iu
<Some(node-uuid,event-uuid)><positive-bool><u64be-magnitude>
Value: empty
压实 Key:
同一前缀 + <None><positive-bool><u64be-magnitude>
Value: empty
代际 Key:
同一索引根 + !iv
Value: u64be
写路径追加正/负增量 Key;读路径扫描 !iu 前缀并求和;压实路径用 !iv 代际做并发保护,删除已观察增量后写基线。
5.4 FullText 索引
共同根:
text
/*<ns>*<db>*<table>\0+<ix>
| 子 Key | Value | 作用 |
|---|---|---|
!id<RIDK> |
u64be |
RecordIdKey → DocId |
!ii<u64be-docid> |
RecordIdKey,revision | DocId → RecordIdKey |
!is<node-uuid> |
SequenceState,revision | 每节点 DocId 序列状态 |
!ib<i64lex-start> |
BatchValue,revision | DocId 批次 |
!dl<u64be-docid> |
u64be |
文档长度 |
!dc |
DocLengthAndCount,revision | 压实后的文档数/总长度 |
!dc<docid><node><update> |
DocLengthAndCount,revision | 文档统计增量 |
!dv |
u64be |
文档统计压实代际 |
!td<term>\0 |
RoaringTreemap 原生 Value | term 的压实 DocId 集合 |
!td<term>\0<docid> |
TermDocument,revision | 频次与位置 |
!tt<term>\0<docid><node><update><add> |
原始空字符串 Value | term/doc 增删事件 |
!tv |
u64be |
term 增量压实代际 |
Key/Value 关系: 高频查询集合使用 Roaring Value;并发写先追加带 UUID 的事件 Key;压实器合并事件到根 Value,并用独立代际 Value 防止旧计划覆盖新写入。TermDocument Value 单独保留频次和位置,服务评分与高亮。
5.5 HNSW 索引
| 子 Key | Value | 作用 |
|---|---|---|
!hd |
HnswDocsState,revision | DocId 分配总体状态 |
!hd<u64be-docid> |
RecordIdKey,revision | DocId → RecordIdKey |
!hi<RIDK> |
u64be |
RecordIdKey → DocId |
!he<element-id> |
SerializedVector,revision | Element → 向量 |
!hn<layer><element-id> |
原始字节 | 当前邻接表 |
!hl<layer><chunk> |
原始字节 | 旧分块邻接表兼容读取 |
!hv<serialized-vector> |
ElementDocs,revision | 精确向量 → 文档集合 |
!hh<hash32> |
ElementHashedDocs,revision | 向量哈希 → 文档集合 |
!hs |
HnswState,revision | 入口点、层级等图状态 |
!hg |
u64be |
压实代际 |
!hr<RIDK> |
HnswRecordPendingUpdate,revision | 当前按记录合并的待处理更新 |
!hp... |
VectorPendingUpdate,revision | 旧追加式待处理区间,兼容排空 |
当前写路径使用 !hr,压实/查询仍先读旧 !hp 再读 !hr,保证滚动升级或遗留数据不会丢失。
5.6 DiskANN 索引
DiskANN 仅在当前这类 64 位非 WASM 目标编译。
| 子 Key | Value | 作用 |
|---|---|---|
!dd |
DiskAnnDocsState,revision | DocId 分配总体状态 |
!dd<u64be-docid> |
RecordIdKey,revision | DocId → RecordIdKey |
!di<RIDK> |
u64be |
RecordIdKey → DocId |
!de<element-id> |
DiskAnnElement,revision | 向量元素 |
!dn<element-id> |
DiskAnnNode,revision | 图节点邻接 |
!dq<serialized-vector> |
DiskAnnElementDocs,revision | 精确向量 → 文档集合 |
!dh<hash32> |
DiskAnnElementHashedDocs,revision | 向量哈希 → 文档集合 |
!ds |
DiskAnnState,revision | 图总体状态 |
!dg |
u64be |
压实代际 |
!dw<shard><RIDK> |
DiskAnnRecordPendingUpdate,revision | 当前分片待处理更新 |
!dy<shard> |
DiskAnnPendingState,revision | 当前分片待处理守卫 |
!dr<RIDK> |
DiskAnnRecordPendingUpdate,revision | 旧非分片待处理更新,兼容排空 |
!dp<shard> |
DiskAnnPendingState,revision | 旧待处理守卫,不再由生产写路径创建 |
当前格式用 !dw/!dy 分片,使压实可按 shard 排空,并避免新旧节点在滚动升级时误清除彼此看不见的待处理状态。读取和压实仍双读 !dr,直至旧区间为空。
5.7 在线索引构建与回放
在线构建不能只扫描一次表,因为扫描期间仍可能发生写入。当前实现用以下 KV 保存构建阶段与重放日志:
| Key | Value |
|---|---|
索引根下 !ig<appending-id><batch-id> |
Appending,revision |
索引根下 !ip<RIDK> |
PrimaryAppending,revision |
表根下 !bs<ix> |
IndexBuildState,revision |
表根下 !br<ix><generation><ticket> |
IndexBuildReservation,revision |
表根下 !bg<ix><generation><ticket><mutation-seq> |
Appending,revision |
表根下 !bp<ix><generation><RIDK> |
PrimaryAppendingTicket,revision |
根空间 /!ic<ns><db><table><ix><node><uuid> |
空 Value |
Key/Value 关系: generation、ticket 和 mutation sequence 进入 Key,便于按构建轮次和顺序扫描;Value 保存要重放的具体索引变更或预约状态。
6. 设计原因、收益与代价
以下为基于真实读写链路的机制分析,不是额外的源码事实声明。
6.1 数值 ID 与名称混合的数据路径
- 当前选择:Namespace 和 Database 名称先经 Catalog 解析为 ID,Index 数据使用 IndexId;TableName 仍直接进入记录、图、引用和索引 Key。
- 收益:高层作用域和索引名不在每条热 Key 中重复;保留 TableName 又使表前缀直观,并允许直接按表名构造扫描范围。
- 代价:TableName 会放大每条记录和索引 Key;首次解析 Namespace/Database 名称需要 Catalog 查找和缓存。
- 可选方案:全部使用名称会简化调试但进一步放大 Key;Table 也使用 TableId 可缩短 Key,却需要稳定的名称反查和重命名语义。
6.2 一记录一 Value
- 收益:读取完整文档只做一次 KV 点查;嵌套对象天然原子更新;非常适合文档模型。
- 代价:只修改一个字段仍重写整个 Value;宽记录会放大写入和复制。
- 可选方案:按字段拆 KV 可降低局部写放大,但增加点查次数、事务键数和快照组装成本。
6.3 索引值进入 Key
- 收益:范围、前缀、等值扫描直接利用底层字节顺序,不依赖后端理解 SurrealQL 类型。
- 代价:长字符串、复合列和复杂 RecordId 会放大 Key;每次字段更新需要删除旧 Key 再写新 Key。
- 可选方案:固定哈希 Key 更短,但失去范围排序;Value 中保存列值则需要额外索引树。
6.4 增量 Key + 压实 Value
- 收益:COUNT、全文和向量更新避免所有写者争用单一热点 Value;UUID 或分片使并发追加可组合。
- 代价:读路径要合并未压实增量,后台还需代际、条件删除和恢复逻辑。
- 可选方案:同步更新单一状态 Value 实现简单,但高并发下冲突和重试显著增加。
6.5 空 Value 的关系数据
- 收益:图邻接、REFERENCE、COUNT 增量把所有查询字段放入 Key,扫描无需解码 Value。
- 代价:Key 更长,更新关系会写多条 KV。
- 可选方案:一个 Value 保存完整邻接集合能缩短 Key,但每次增删边都会重写并争用大集合。
7. KV 数据库启示
- 先定义统一的有序类型编码。 数字、字符串终止与转义、Option、容器边界必须在所有索引和范围扫描中一致。
- 把 Key 设计成可证明的前缀层级。 每类对象都应有明确根、点查 Key 和
[prefix+00, prefix+ff)扫描边界。 - 显式分开身份和内容。 Record Key 作为权威身份,可以修复或覆盖 Value 中陈旧的 ID。
- 明确选择哪些层级使用稳定 ID。 当前 Namespace、Database、Index 进入热 Key 时使用 ID,而 Table 保留名称;自研系统应按 Key 放大、可读性和重命名成本作同样的显式取舍。
- 热点聚合改为追加增量。 计数、倒排集合、ANN 更新可用"增量 Key + 压实 Value + generation CAS"。
- 滚动升级要保留双读排空窗口。 新格式写新前缀,读取/压实同时理解旧前缀,直到旧区间为空。
- 删除任务放在目标前缀之外。 异步回收状态若位于待删前缀内,会随第一阶段删除而丢失。
- 从一开始就区分逻辑 MVCC 与应用层历史。 Change Feed 应是独立有序 KV,不应与后端内部版本后缀混为一谈。
- 为大 Value 预留演进策略。 当前记录没有分块;自研系统若面向超大文档,应提前设计对象存储引用、分块清单或阈值迁移。