万物皆可KV(2)SurrealDB 存储布局分析

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 的字段顺序支持以下前缀扫描:

  1. 目标记录的全部引用。
  2. 目标记录中来自某张源表的引用。
  3. 目标记录中来自某张源表某字段的引用。

未声明 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 数据库启示

  1. 先定义统一的有序类型编码。 数字、字符串终止与转义、Option、容器边界必须在所有索引和范围扫描中一致。
  2. 把 Key 设计成可证明的前缀层级。 每类对象都应有明确根、点查 Key 和 [prefix+00, prefix+ff) 扫描边界。
  3. 显式分开身份和内容。 Record Key 作为权威身份,可以修复或覆盖 Value 中陈旧的 ID。
  4. 明确选择哪些层级使用稳定 ID。 当前 Namespace、Database、Index 进入热 Key 时使用 ID,而 Table 保留名称;自研系统应按 Key 放大、可读性和重命名成本作同样的显式取舍。
  5. 热点聚合改为追加增量。 计数、倒排集合、ANN 更新可用"增量 Key + 压实 Value + generation CAS"。
  6. 滚动升级要保留双读排空窗口。 新格式写新前缀,读取/压实同时理解旧前缀,直到旧区间为空。
  7. 删除任务放在目标前缀之外。 异步回收状态若位于待删前缀内,会随第一阶段删除而丢失。
  8. 从一开始就区分逻辑 MVCC 与应用层历史。 Change Feed 应是独立有序 KV,不应与后端内部版本后缀混为一谈。
  9. 为大 Value 预留演进策略。 当前记录没有分块;自研系统若面向超大文档,应提前设计对象存储引用、分块清单或阈值迁移。
相关推荐
讯捷蓝达4 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
且从容.5 小时前
ubuntu安装flink
linux·ubuntu·flink
数字新视界6 小时前
动环监控信创适配在智能机房管理中的应用价值与未来展望
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
爱和冰阔落6 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨6 小时前
WSL2的安装与使用
linux·wsl
微小的鱼6 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng7 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮7 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
上火的金鱼妹8 小时前
k8s生产实际问题排查(网关,数据库读写等)
运维·服务器
Linux-lucky19 小时前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu