万物皆可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=1DatabaseId=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 不存在"的条件写。若组合中含 NONENULL,实现会退化为带 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=0x02Some=0x03;Bool 使用 false=0x02true=0x03
  • 字符串和字节片中的 0x00/0x010x01 前缀转义,再写 0x00 终止符。
  • Array、Set、Object 递归编码;元素或字段结束后再写容器终止符。
  • 普通 storekey 格式中的 Value::Number 同时保存规范数值和 NumberKind,因此保留 Int/Float/Decimal 原始种类;RecordIdKey::Number 本身只接受 i64。
  • 索引专用 IndexFormat 只保留规范数值,忽略 NumberKind;数值相等的 00.00dec 产生相同索引 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_buildalpha、是否使用向量哈希。
  • 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 预留演进策略。 当前记录没有分块;自研系统若面向超大文档,应提前设计对象存储引用、分块清单或阈值迁移。
相关推荐
万事可爱^1 小时前
Claude 新发布的 Opus 5,系统提示语删了 80%,半价还能逼近 Fable 5
android·服务器·数据库·人工智能·claude
无小道2 小时前
深入解析操作系统文件缓冲区:页缓存、基数树与f_pos的协同设计
linux·缓存·文件缓冲区
其实防守也摸鱼2 小时前
GitHub开源项目破圈方法论:从技术自嗨到生态共赢
服务器·数据库·学习·开源·github·命令行·linux系统
爱写代码的森3 小时前
鸿蒙三方库 | harmony-utils之PasteboardUtil剪贴板数据读写详解
服务器·华为·harmonyos·鸿蒙·huawei
AOwhisky3 小时前
Linux(CentOS)系统管理入门笔记(第十二期)——系统管理工具与软件包管理(上篇):Cockpit 与 RPM 包管理
linux·运维·笔记·centos·云计算
寒水馨3 小时前
Linux下载、安装godot-4.7.1-stable(附安装包Godot_v4.7.1-stable_linux.x86_64.zip)
linux·游戏引擎·godot·游戏开发·2d游戏·3d游戏·godot engine
念恒123063 小时前
Socket编程UDP(中)
linux·网络协议·udp
深耕运维十八载云架构实务3 小时前
系统故障玄学之内存明明显示空闲,服务器却频繁卡顿卡死?拆解被忽略的 Linux 隐性内存陷阱
linux
qetfw4 小时前
CentOS 7 搭建 Sendmail + Dovecot 邮件服务器:SMTP、POP3、IMAP 与 TLS
linux·centos