1. 总体布局
text
SQL 对象或一行数据
│
├─ Schema 元数据
│ ├─ Key: "m" + 结构前缀 + 对象逻辑名/字段名
│ └─ Value: JSON、十进制文本、原始字符串或空
│
├─ 主记录
│ ├─ Key: "t" + 物理表/分区 ID + "_r" + 行句柄
│ └─ Value: row v1 或 row v2;只保存非句柄、非虚拟生成列
│
├─ 行存索引
│ ├─ Key: "t" + 表/分区 ID + "_i" + 索引 ID + 有序列值 [+ 行定位符]
│ └─ Value: 行定位符、分区 ID、恢复数据及兼容标记,或单字节占位
│
└─ 事务与命名空间层
├─ Key: API V1 保持不变;API V2 前置 0x78 + 3 字节 keyspace ID
└─ Value: keyspace codec 不修改;MVCC 以 Put/Delete 和时间戳管理版本
这里最重要的边界是:
- Key 决定对象命名空间、排序、点查和范围扫描边界。
- Value 保存不能或不宜放入排序 Key 的对象内容、非主键列和行定位附加信息。
- Key/Value 关系 是"一个可排序身份对应一个对象版本或索引入口",而不是把
完整 SQL 行简单序列化到一个不透明键名下。
2. 由当前生产编码器生成的真实样例
以下字节不是手工拼接。它们由当前生产编码器生成,再由对应生产解码器反向解析。
转义表示便于观察 ASCII 边界,Hex 是完整字节。
2.1 数据库元数据:数据库 42 app
逻辑对象是 ID 为 42 的数据库。它保存在 DBs 哈希的 DB:42 字段。
Key
text
转义: "mDBs\x00\x00\x00\x00\x00\xfa\x00\x00\x00\x00\x00\x00\x00hDB:42\x00\x00\x00\xfc"
Hex: 6d4442730000000000fa000000000000006844423a3432000000fc
Value
json
{"id":42,"db_name":{"O":"app","L":"app"},"charset":"utf8mb4","collate":"utf8mb4_bin","Deprecated":{},"state":5,"policy_ref_info":null}
text
Hex: 7b226964223a34322c2264625f6e616d65223a7b224f223a22617070222c224c223a22617070227d2c2263686172736574223a22757466386d6234222c22636f6c6c617465223a22757466386d62345f62696e222c2244657072656361746564223a7b7d2c227374617465223a352c22706f6c6963795f7265665f696e666f223a6e756c6c7d
Key/Value 关系 :Key 唯一定位数据库元数据字段;134 字节 Value 是完整数据库
定义 JSON。反向解码得到哈希名 DBs、字段 DB:42、数据库 ID 42 和名称 app。
2.2 row v2:表 100、句柄 42
示例逻辑行是:
text
id=42(聚簇主键,不重复写入 Value)
name="Alice"
score=-7
note=NULL
列 ID 分别为 1、2、3、4;note 的显式 NULL 需要保留。
Key
text
转义: "t\x80\x00\x00\x00\x00\x00\x00d_r\x80\x00\x00\x00\x00\x00\x00*"
Hex: 7480000000000000645f72800000000000002a
Value
text
转义: "\x80\x00\x02\x00\x01\x00\x02\x03\x04\x05\x00\x06\x00Alice\xf9"
Hex: 80000200010002030405000600416c696365f9
逐段解释 Value:
text
80 row v2 版本字节
00 flags:小列 ID、小偏移、无行校验和
02 00 2 个非 NULL 列(小端)
01 00 1 个 NULL 列(小端)
02 03 非 NULL 列 ID:2、3
04 NULL 列 ID:4
05 00 06 00 累计数据结束偏移:5、6
41...65 "Alice"
f9 -7 的最短一字节补码
Key/Value 关系 :Key 中的表 ID 100 和句柄 42 唯一定位主记录;Value 只保存列
2、3、4 的状态。反向解码恢复 name="Alice"、score=-7、note=NULL,并由
Key 恢复 id=42。
同一逻辑 Key 在 API V2、keyspace ID 0x010203 下的物理 Key 是:
text
780102037480000000000000645f72800000000000002a
前 4 字节 78 01 02 03 只属于物理 Key 命名空间;Value 保持不变。
2.3 唯一索引:name="alice" 指向句柄 42
索引属于表 100,索引 ID 为 1,没有 NULL,因此是 distinct 唯一入口。
Key
text
转义: "t\x80\x00\x00\x00\x00\x00\x00d_i\x80\x00\x00\x00\x00\x00\x00\x01\x01alice\x00\x00\x00\xfc"
Hex: 7480000000000000645f69800000000000000101616c696365000000fc
Value
text
Hex: 000000000000002a
Key/Value 关系 :Key 在索引值后不再附加句柄,因此相同 alice 只能对应一个
入口;8 字节大端 Value 保存整数句柄 42。反向解码从 Key 得到表 100、索引 1、
值 alice,从 Value 得到句柄 42。
2.4 非唯一索引:name="alice" 与句柄 42 共同唯一
索引属于表 100,索引 ID 为 2。
Key
text
转义: "t\x80\x00\x00\x00\x00\x00\x00d_i\x80\x00\x00\x00\x00\x00\x00\x02\x01alice\x00\x00\x00\xfc\x03\x80\x00\x00\x00\x00\x00\x00*"
Hex: 7480000000000000645f69800000000000000201616c696365000000fc03800000000000002a
Value
text
转义: "0"
Hex: 30
Key/Value 关系 :句柄 42 附在 Key 末尾,使多个同值行能够共存并按句柄排序;
Value 只是兼容占位字节。反向解码能从 Key 单独恢复索引值和句柄。
3. Schema 到 KV
3.1 结构化元数据 Key
所有这里讨论的 Meta Key 都以 ASCII m 开头。其后有两种主要形式。
字符串项:
text
Key = "m" | EncodeBytes(logical-key) | EncodeUint('s')
Value = 调用方原始字节
哈希字段:
text
Key = "m" | EncodeBytes(hash-key) | EncodeUint('h') | EncodeBytes(field)
Value = 调用方原始字节
EncodeBytes 把输入按 8 字节分组,每组补零后增加一个 0xff-padCount 标记;
长度恰好为 8 的倍数时仍增加终止组。因此 Key 可无歧义解码,并保持字典序。
EncodeUint 是 8 字节大端无符号整数;上面的 's'、'h' 实际占其最低字节。
旧版本曾有哈希元计数 Key。当前写路径不维护它,哈希长度由字段前缀扫描得到;
列表的 L/l 结构编码仍是通用兼容能力,但当前 Schema 主路径没有生产调用者。
3.2 Schema 对象矩阵
| 对象 | Key | Value | Key/Value 关系与生命周期 |
|---|---|---|---|
| 数据库 | DBs 哈希,字段 DB:<dbID> |
完整 DBInfo JSON | Key 按数据库 ID 定位;更新重写 Value;删除移除字段 |
| 表、普通视图、序列定义 | DB:<dbID> 哈希,字段 Table:<tableID> |
完整 TableInfo JSON | Key 体现所属数据库和对象 ID;更新定义重写整个 Value |
| 列、索引定义、外键、检查/唯一约束、分区、视图 SQL、序列选项 | 无独立 Key | 嵌入 TableInfo JSON | 与表定义原子读取;局部修改具有整 Value 重写成本 |
| 表/索引/自增/自随机/序列数值 | DB:<dbID> 哈希中的 TID:、IID:、TARID:、SID:、SequenceCycle: 字段 |
十进制 ASCII 整数 | Key 区分分配器种类和表 ID;Value 支持原子增量语义 |
| 放置策略 | Policies 哈希,字段 Policy:<id> |
魔数 0x00 + PolicyInfo JSON |
Key 按策略 ID 定位;表/库 JSON 只保存引用 |
| 数据脱敏策略 | MaskingPolicies 哈希,字段 MaskingPolicy:<id> |
魔数 0x00 + JSON |
Key 按策略 ID 定位;定义集中保存 |
| 资源组 | ResourceGroups 哈希,字段 RG:<id> |
魔数 0x00 + ResourceGroupInfo JSON |
Key 按资源组 ID 定位 |
| Schema 全局版本 | 字符串逻辑 Key SchemaVersionKey |
十进制 ASCII 版本 | 每次 Schema 变更推进 Value |
| Schema diff | 字符串逻辑 Key Diff:<version> |
SchemaDiff JSON | Key 中版本可点查;Value 描述该版本变化 |
| 全局 ID 与策略 ID 分配器 | 多个固定字符串逻辑 Key | 十进制 ASCII 整数 | Key 区分分配器;Value 原子递增 |
| 引导与表版本 | 固定字符串逻辑 Key | 十进制 ASCII 或状态字符串 | Key 定位单例状态 |
| 元数据锁、BDR 角色、Schema cache 大小、摄取调优等 | 固定字符串逻辑 Key,或专用哈希字段 | 0/1、原始字符串、十进制文本或 JSON |
Key 定位集群级配置;Value 按配置类型解释 |
| Request Unit 统计 | 固定字符串逻辑 Key | JSON | 单 Key 保存一组统计状态 |
| DDL job 历史 | DDLJobHistory 哈希,字段为 8 字节大端原始 job ID |
Job JSON | Key 按 job ID 排序定位;完成后写入历史 |
| 分布式执行框架调优 | DXFScheduleTune 哈希,字段为 keyspace 字符串 |
调优 JSON | Key 按 keyspace 区分配置 |
| 运行中 DDL、reorg、MDL 和部分历史 | mysql.tidb_ddl_* 系统表的行/索引 Key |
普通系统表 row Value | 不使用独立 Meta 编码,服从第 5 节的标准表 KV |
| 统计信息、权限、角色、绑定等 | mysql.* 系统表的行/索引 Key |
普通系统表 row Value | Schema 只是系统表定义,业务对象内容按标准表 KV 保存 |
TableInfo Value 是 Schema 信息最密集的单元。它包含列顺序、类型、字符集/排序
规则、默认值、生成表达式和依赖、隐藏列、在线变更状态;索引列与前缀、唯一性、
主键/全局/多值/部分/列式属性;外键、检查约束和唯一约束;分区定义;视图或
序列定义;聚簇句柄模式、自增与自随机选项、TiFlash 副本、临时表、缓存表、
placement 引用、统计选项、交换分区、TTL、软删除、亲和性和 region split
策略等。数据库 ID 不重复嵌入这个 Value,而由 Meta Key 的数据库哈希部分给出。
当前生产 Meta/DDL 持久化分派没有为存储过程、存储函数或触发器定义独立目录
Key/Value。解析器中的兼容 AST 和错误码不等于已经存在持久化对象,因此本文
不把它们虚构为 Meta KV;将来若通过系统表实现,其内容也会先服从普通表
Key/Value,除非另行增加专用 Meta 分派。
列没有独立 KV 的直接收益是读取表定义时只做一次点查,并能得到一致快照;代价是
新增列、改变一个默认值或更新一个索引状态都要重写整个 TableInfo Value。删除表
定义时删除对应哈希字段;删除数据库还会清理该数据库的表定义哈希。
逻辑"数据库"是 Schema 命名空间;用户表的物理行 Key 不包含数据库名或数据库
ID,而依靠全局唯一的表/分区 ID 隔离。API V2 的 keyspace 是更外层租户式物理
命名空间,它统一前置到 Meta Key、行 Key 和索引 Key,不嵌入各自 Value。
4. Data 到 KV
4.1 主记录 Key
主记录 Key 的格式是:
text
Key = 't'
| EncodeInt(physical-table-or-partition-id)
| "_r"
| handle.Encoded()
EncodeInt 把有符号整数的符号位翻转后用 8 字节大端保存,因此负数、零、正数
按数值顺序排列。非分区表使用表 ID;分区表使用实际分区的物理 ID。
整数聚簇句柄的编码是一个 EncodeInt。复合聚簇句柄把各主键列按 comparable
datum 规则连续编码;内部会把很短的 common handle 补到至少 9 字节,以维持
句柄分类和解析约束。主键列已经存在于 Key,因此不会再写入主记录 Value。
读取一行时以完整 Key 点查;表扫以 t|tableID|_r 为前缀构造起止 Key。Key 的
有序性让同一物理表的记录连续,也让整数或复合聚簇主键范围可以直接映射为 KV
范围。
4.2 主记录 Value:row v1
row v1 仍需读取,且可由升级集群或显式配置继续写入。它是重复的
列 ID datum | 列值 datum:
text
Value = EncodeValue(colID1) | EncodeValue(value1)
| EncodeValue(colID2) | EncodeValue(value2)
| ...
列 ID 和 Value 都有类型 flag。常用 Value 分支是:
- 整数 Value :有符号使用 varint flag
0x08,无符号使用 uvarint flag
0x09,后接变长整数。 - 字符串/字节 Value :compact-bytes flag
0x02,后接变长长度和原始字节。 - 浮点 Value :float flag
0x05,后接 8 字节可排序浮点编码。 - 十进制 Value :decimal flag
0x06,后接精度、标度和十进制二进制体。 - 时间 Value :uint flag
0x04,后接打包时间。 - 时长 Value :duration flag
0x07,后接有序有符号纳秒数。 - JSON Value :JSON flag
0x0a,后接 JSON 类型码和二进制 JSON。 - NULL Value :nil flag
0x00。
空逻辑行的 Value 是单个 nil flag 00,而不是零长度 Value;零长度在事务
mutation 中有删除语义,不能拿来表示一条存在的空行。
row v1 的收益是自描述、兼容性强;代价是每一列和值都重复携带 flag,列 ID
也使用通用 datum 编码,空间与解码分支多于 row v2。
4.3 主记录 Value:row v2
新建集群默认采用 row v2,DDL 重组默认也使用 v2。其总体格式为:
text
Value =
0x80
| flags
| nonNullCount:u16le
| nullCount:u16le
| sorted non-null column IDs
| sorted null column IDs
| cumulative end offsets of non-null payloads
| concatenated non-null payloads
| optional checksum header and crc32
flags 的 bit 0 表示 large 格式,bit 1 表示带行校验和。小格式列 ID 是 1 字节、
偏移是 2 字节小端;只要有列 ID 大于 255 或数据区超过 65535 字节,就切换为
4 字节小端列 ID 和 4 字节小端偏移。NULL 列只进入 NULL ID 数组,没有数据体。
非 NULL 列 ID、NULL 列 ID 都升序保存,偏移数组给出每段数据的结束位置。因此
解码器可以二分列 ID,只切取需要的列 Value,而不必逐列解析前缀。row v2 外壳
自身没有通用压缩层;字符串、BLOB、JSON、向量等都内联在同一个主记录 Value。
可选行校验和只用于 row v2,并受会话和内部事务条件控制,默认关闭。当前版本的
校验和覆盖已经形成的 row Value 头部/数据以及行句柄,保存 32 位小端 CRC32。
写入时会跳过:
- 已经编码进 Key 的句柄列;
- 虚拟生成列;
- 可以由 Schema 默认值无歧义恢复、且当前等于 NULL 的列。
存储生成列仍写 Value。读取旧行遇到缺失列时,由当前 Schema 补默认值;如果
显式 NULL 与"列缺失后补默认值"语义不同,编码器会把该列列入 NULL ID 数组。
4.4 当前 SQL 类型的字节映射
下表的"行 Value"指主记录 Value 内某一列的数据体;row v1 还会在数据体外加
类型 flag,row v2 依靠 Schema 类型与偏移解释数据体。"索引 Key"指该类型
合法作为普通行存索引列时的 comparable 编码。
| SQL 类型 | 行 Value | 索引 Key | 说明 |
|---|---|---|---|
BOOL、BOOLEAN |
按 TINYINT(1) 整数保存 |
按有符号整数保存 | 只是语法别名,没有独立类型码或 Value 格式 |
TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT |
v1 为 varint;v2 按数值范围取 1/2/4/8 字节小端补码 | flag 0x03 + 8 字节大端符号位翻转 |
声明宽度不决定 row v2 固定宽度 |
上述整数的 UNSIGNED 形式 |
v1 为 uvarint;v2 取 1/2/4/8 字节小端无符号值 | flag 0x04 + 8 字节大端无符号值 |
最大值仍保持有序 |
YEAR |
与整数相同 | 与整数相同 | 没有独立行数据体 |
FLOAT、DOUBLE |
v1/v2 都使用 8 字节浮点有序变换 | flag 0x05 + 同一 8 字节变换 |
正数翻转符号位,负数逐位取反,使字节序等于数值序 |
DECIMAL(p,s) |
精度 1 字节、标度 1 字节、MySQL 二进制定点数 | flag 0x06 + 可排序十进制编码 |
负数数据位取反并规范最高位;保持精确十进制语义 |
CHAR、BINARY |
v1 为 compact bytes;v2 为原始字节 | flag 0x01 + 8 字节分组转义串 |
CHAR 的补空格与排序规则先由类型语义处理 |
VARCHAR、VARBINARY |
v1 为 compact bytes;v2 为原始字节 | flag 0x01 + 8 字节分组转义串 |
新排序规则下,字符类型的 Key 保存 sort key,不一定是原文 |
TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT |
与字符串相同,内联主记录 Value | 合法前缀索引保存截断后的 sort key/字节 | 不能把完整大文本误解为独立 overflow KV |
TINYBLOB、BLOB、MEDIUMBLOB、LONGBLOB |
原始字节内联主记录 Value | 合法前缀索引保存截断字节 | 当前行 codec 没有 BLOB chunk Key |
DATE、DATETIME |
打包为无符号时间整数;v2 使用最短 1/2/4/8 字节小端 | flag 0x04 + 8 字节大端打包值 |
Key 保持时间顺序 |
TIMESTAMP |
写前按时区转换到 UTC,再按打包时间保存 | 按规范化后的打包时间形成 Key | 解码展示时再按会话时区转换 |
TIME |
纳秒数;v1 为有序时长编码,v2 为最短 1/2/4/8 字节小端补码 | flag 0x07 + 有序有符号整数 |
可表达负时长 |
ENUM、SET |
数值表示;v2 为最短无符号小端 | 按无符号数形成有序 Key | Schema Value 保存元素文本和排序规则 |
BIT |
无符号数;v2 为最短无符号小端 | 按无符号数形成有序 Key | 位宽在 Schema 中 |
JSON |
JSON 类型码 + MySQL binary JSON | 普通 JSON 列不能直接做普通 B-tree Key | 多值索引只编码从 JSON 数组展开出的标量 |
VECTOR(FLOAT32) |
dimension:u32le + 连续 float32le |
不写普通 TiKV 行存索引 Key | 向量索引是列式索引;维度上限 16383,拒绝 NaN/Inf |
SQL NULL 值 |
v1 用 0x00;v2 放入 NULL 列 ID 数组 |
comparable nil flag 0x00 |
唯一索引含 NULL 时不视为 distinct,句柄进入 Key |
binary JSON Value 的第一字节是类型码。对象和数组含 32 位小端元素数与总长度,
随后是键/值入口和偏移指向的数据区;数字采用小端定宽表示,字符串用变长长度,
并支持字面量、对象、数组和 opaque 数据。JSON 中的 list/map 由这一 Value
内部结构表达,不生成独立行 Key。
语法别名不会增加 Value 格式:
MIDDLEINT、INT1/2/3/4/8、INTEGER分别归一到相应整数类型;
SERIAL是BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE的列定义简写。NUMERIC、FIXED归一到DECIMAL;FLOAT4归一到FLOAT,
FLOAT8和DOUBLE PRECISION归一到DOUBLE;REAL根据 SQL mode
归一到FLOAT或DOUBLE。CHARACTER、NCHAR、NATIONAL CHAR等归一到CHAR;
CHARACTER VARYING、NVARCHAR、NATIONAL VARCHAR等归一到
VARCHAR。LONG、LONG VARCHAR归一到MEDIUMTEXT,LONG VARBINARY归一到
MEDIUMBLOB,SQL_TSI_YEAR归一到YEAR。- MariaDB 解析模式下的
UUID没有原生 TiDB 类型,而是归一为CHAR(36)。
SIGNED、UNSIGNED和ZEROFILL只改变整数属性,其中ZEROFILL
隐含UNSIGNED。
当前语法没有独立的 SQL array、map、struct、tuple、domain、interval 或带时区
时间列格式。多值索引里的"数组"是把 JSON 数组 cast 为元素数组的索引标记,
不是新的主记录 Value 类型。类型系统中的 TypeUnspecified、TypeNull、
TypeNewDate、TypeGeometry 是协议、表达式或兼容常量;TypeVarString
也是协议/兼容字符串类型,当前普通 VARCHAR DDL 使用 TypeVarchar。它们
都不要求虚构新的持久化 KV 格式。
4.5 排序规则与可恢复字符串
字符串进入普通索引 Key 时,新排序规则模式保存 collation sort key,以保证
KV 字典序与 SQL 比较顺序一致。这会丢失大小写、重音或尾空格等原文差异。需要
从索引覆盖扫描恢复原值时,索引 Value 可附带一段 row v2 格式的"恢复数据";
二进制字符串或无需恢复的值不写这段数据。
因此:
- 索引 Key负责比较和范围边界,可能只含规范化 sort key。
- 索引 Value在需要时补充原始字符串或尾空格信息。
- Key/Value 关系同时满足"按 SQL 语义排序"和"覆盖索引能恢复展示值"。
5. 索引到 KV
5.1 通用行存索引 Key
text
Key = 't'
| EncodeInt(index-table-or-partition-id)
| "_i"
| EncodeInt(index-id)
| EncodeKey(indexed-value-1, ...)
| optional row locator
索引列用 comparable datum 编码。常用 flag 是 NULL 0x00、bytes 0x01、
signed int 0x03、unsigned int 0x04、float 0x05、decimal 0x06、
duration 0x07;编码确保合法索引类型按 SQL 比较语义排列。
distinct = unique && 所有索引列都非 NULL:
- distinct 唯一入口不把行定位符放进 Key,而把它放进 Value,以便相同索引值
发生 Key 冲突。 - 非唯一入口和含 NULL 的唯一入口把行定位符附在 Key 末尾,以允许多个相同值。
- 整数定位符在 Key 中是 flag
0x03加EncodeInt(handle);common handle
直接附其复合有序编码。
5.2 索引 Value
兼容的最短形式是:
- 唯一、整数句柄、局部索引 Value:8 字节原始大端无符号句柄。
- 非唯一索引 Value :ASCII
0,即0x30,真实句柄已在 Key。
扩展格式以尾部长度描述可选段,并可组合:
text
common handle: 0x7f | handleLength:u16be | handle bytes
global index: 0x7e | EncodeInt(partitionID)
restored data: row v2 restore segment
compatibility: padding、int handle 或 uncommitted 标记
扩展 Value 通常补到至少 10 字节以与旧格式可靠区分。旧 common-handle v1
Value 仍可读取。全局索引 v1 的某些非 distinct、非聚簇形式还会在 Key 的内部
句柄前编码 0x7e|partitionID,并在 Value 中保留分区 ID;这是兼容格式,不应
误认为所有当前索引都采用相同重复布局。
5.3 索引种类矩阵
| 索引种类 | Key | Value | Key/Value 关系、收益与代价 |
|---|---|---|---|
| 整数聚簇主键 | 主记录 Key 的句柄部分 | 主记录 Value 不重复主键 | 一次点查得到行;改变主键等价于迁移主记录 Key |
| 复合聚簇主键 | 主记录 Key 的 common handle | 主记录 Value 不重复主键 | 天然支持主键前缀/范围;宽主键放大所有二级索引定位符 |
| 非聚簇主键 | 标准唯一二级索引 Key | Value 保存行句柄 | 先查主键索引再查主记录 |
| 唯一索引、无 NULL | 索引值到 Key 末尾 | Value 保存行定位符 | Key 冲突强制唯一;点查一步得到句柄 |
| 唯一索引、含 NULL | 索引值后附行定位符 | 通常为占位或扩展 Value | 遵循多个 NULL 可共存语义 |
| 非唯一索引 | 索引值后附行定位符 | 0x30 或扩展 Value |
相同值按句柄有序;范围结果可直接得到定位符 |
| 局部索引 | Key 使用物理分区 ID | Value 通常不需分区 ID | 每分区连续且更短;跨分区查询需扫描多个前缀 |
| 全局索引 | Key 使用逻辑表 ID | Value 保存物理分区 ID 和行定位符 | 跨分区唯一/点查只访问一个索引空间;Value 更宽,分区变更更复杂 |
| 前缀索引 | Key 只保存截断值 | Value 按需保存恢复数据 | 控制 Key 尺寸;碰撞增多,可能需回表过滤 |
| 表达式索引 | 表达式结果先成为隐藏生成列,再写标准索引 Key | 标准索引 Value | 复用普通索引格式;Schema Value 承担表达式与隐藏列定义 |
| 部分索引 | 谓词为真时才写标准索引 Key | 标准索引 Value | 降低空间和写放大;优化器必须证明谓词适用 |
| 多值索引 | 将一个 cast JSON 数组展开为多个标准索引 Key | 每个入口使用标准索引 Value | 一个主记录对应 0...N 个索引 KV;同一数组内按 binary JSON 哈希去重,空数组写 0 个,NULL 写 1 个 |
| 临时索引 | 索引 ID 与 0x7fff000000000000 做位组合,Key 仍用索引前缀 |
Value 是 normal/delete 元素序列及 d/b/m 阶段标记 |
在线 DDL 暂存操作;完成切换后不是最终查询布局 |
BTREE |
标准行存索引 Key | 标准索引 Value | 实际依赖有序 KV |
HASH,以及兼容元数据中的 RTREE |
若进入行存索引实现,仍使用标准有序索引 Key | 标准索引 Value | HASH 不产生哈希桶;当前 SPATIAL/RTREE DDL 会被拒绝,兼容枚举也没有空间树 KV |
| hypothetical | 无持久 Key | 无持久 Value | 仅优化器会话模拟,不影响存储 |
VECTOR、INVERTED、FULLTEXT 列式索引 |
普通行 DML 不写 TiKV 索引 Key | 普通行 DML 不写 TiKV 索引 Value | 定义嵌入 TableInfo;物理索引数据由 TiFlash 持有;HNSW 只存在于 AST,预处理后归一为 VECTOR |
普通索引 DESC |
当前与升序使用同一行存 Key | 同一标准 Value | 语法可接受但普通行存编码未反转字节,不能推断为独立降序布局 |
前缀索引对二进制或 ASCII 值按字节截断,对其他字符串按 Unicode 字符截断,
然后再执行排序规则编码。多值索引最多展开一个数组表达式;每个元素与其余普通
索引列组合。表达式索引之所以没有新 KV 格式,是因为表达式结果在 Schema 中
表现为隐藏生成列。
索引点查使用完整 Key;非唯一查找、前缀条件和范围条件以索引前缀构造起止 Key,
上界通常由前缀的字典序后继得到。索引 Key 把表/分区 ID、索引 ID 和列值依次
放在高到低层级,正好让同一索引及相邻列值物理聚集。
6. 写入、更新、删除与 MVCC
插入一行时:
text
1. 由物理表/分区 ID 与句柄构造主记录 Key。
2. 把非句柄列编码为 row v1 或 row v2 Value。
3. 向事务内存缓冲写 Set(record Key, row Value)。
4. 对每个适用的行存索引生成一个或多个 index Key/Value 并 Set。
5. 提交时把非空 Value 变为 Put mutation。
更新一行时,主记录通常在同一 Key 上写入新 Value;索引列、句柄或分区归属变化
时,旧索引 Key/旧主记录 Key 被 Delete,新 Key 被 Put。不变的 Key 在新的
commit timestamp 下形成新版本,而不是原地改写已提交版本。
删除一行时,TiDB 对主记录 Key 和各索引 Key 调用 Delete。事务缓冲中的删除是
空 Value mutation,预写阶段映射成 Op_Del,不是"写一个 row Value 为
NULL"。因此必须区分:
- SQL NULL Value:行仍存在;row v1 用 nil flag,row v2 用 NULL 列 ID。
- 空逻辑行 Value :行仍存在;row v1 至少写单字节
00,row v2 有完整头部。 - Delete mutation:逻辑 Key 在新 MVCC 版本后不可见;不携带用户 Value。
读取在一个快照时间戳上执行。点查先查完整 Key,范围扫描按前缀迭代;MVCC 层
负责选择不晚于快照的最新可见 Put,并让 Delete 截断后续快照的可见性。这里能
确认的是 TiDB/client 的 mutation 与时间戳协议;TiKV server 在 RocksDB 各 CF
中如何编码版本后缀、短 Value、锁和回滚记录,不在当前源码可验证范围内。
API V2 再把每个事务 Key 改为:
text
Physical Key = 0x78 | keyspaceID:u24be | TiDB logical Key
Physical Value = TiDB logical Value
这使不同 keyspace 的整个 Meta、记录和索引范围物理隔离,并保持每个 keyspace
内部原有排序。API V1 不加此前缀。
7. 组织原因、收益与代价
| 设计 | 主要收益 | 明确代价 |
|---|---|---|
| 全局表/分区 ID 进入 Key | 数据库改名、表改名不搬数据;范围天然隔离 | ID 必须稳定且全局管理;从裸 Key 看不到 SQL 名称 |
| 表记录与索引采用不同 ASCII 分隔前缀 | 点查、表扫、索引扫边界简单 | 二级索引和主记录是多个 KV,写事务有写放大 |
| 聚簇句柄进入主记录 Key | 主键点查和范围扫直接映射 KV | 宽复合主键放大记录 Key 与二级索引 |
| row v2 的列 ID/偏移数组 | 随机取列快、固定开销低、NULL 无数据体 | 解码依赖 Schema;大列 ID/大行触发 4 字节目录 |
| Schema 子对象嵌入 TableInfo JSON | 一次点查获得自洽表定义,演进方便 | 小变更重写大 Value;JSON 比专用二进制更占空间 |
| comparable 索引 Key | SQL 排序、范围扫描、唯一冲突复用 KV 顺序 | 排序规则 sort key 可能丢原文,需要 Value 恢复段 |
| 全局索引的分区 ID 放 Value | 跨分区点查和唯一约束只需一个索引空间 | Value 变宽,分区移动和兼容格式更复杂 |
| 大文本、JSON、向量内联主记录 Value | 没有额外 chunk 查找和孤儿对象管理 | 单行变宽,事务 entry 大小和网络/写放大成为约束 |
| MVCC 版本位于底层事务层 | SQL codec 无需为每个对象自行设计版本链 | 裸逻辑 Key/Value 不能单独解释历史与可见性 |
8. KV 设计启示
- 先固定 Key 的排序职责,再选择 Value 的紧凑程度。 身份、范围边界和冲突
检测应放 Key;不参与排序且可能演进的内容放 Value。 - 区分逻辑命名空间和物理租户命名空间。 TiDB 用全局对象 ID 稳定逻辑布局,
再用统一 keyspace 前缀隔离租户,两层不互相污染。 - 为唯一和非唯一入口选择不同的行定位位置。 唯一索引让相同值竞争同一
Key;非唯一索引把定位符放 Key,既消除冲突又稳定排序。 - 显式区分 NULL、缺失、空 Value 和删除。 四者混用会破坏默认值回填、
唯一约束和 MVCC 墓碑语义。 - 紧凑行格式仍要保留可演进目录。 列 ID 与偏移数组使新增列、缺失列默认值
和投影读取可以共存,而无需让每列都携带完整类型标签。 - 比较编码与恢复编码可以分离。 Key 保存 sort key,Value 只在需要时保存
原始文本,能同时满足排序正确性与覆盖索引。 - 兼容格式必须可判别。 索引 Value 的 magic、长度与最小尺寸让新旧格式
可共存;设计自研格式时应预留明确的版本或分支判据。 - 只把已验证边界写成字节规范。 客户端 mutation 语义不能替代服务端 CF
字节证据;依赖未锁定时,应把未知边界列为约束而不是补全想象。