元数据
元数据
TsFile 中的元数据分为两类:
- 与数据一起保存的 Chunk Header、Page Header 和 Page Statistics。
- 数据区之后的 Timeseries Metadata、Metadata Index 和 File Metadata。
Footer 元数据从 SEPARATOR (0x02) 之后开始。TimeseriesMetadata 范围与测量索引节点可以交错出现;索引中的 offset 和 end_offset 决定每个范围的实际边界。所有 offset 都是从文件首字节开始计算的绝对偏移量。
查找数据时,读取器沿下列层级逐步缩小范围,从表和设备定位到可能包含目标值的 Page:
| 层级 | 标识对象 | 主要用途 |
|---|---|---|
| 文件元数据 | 表及设备索引根 | 进入正确的索引树 |
| 设备索引 | 设备 | 定位一个设备的测量索引 |
| 测量索引 | 测量名称区间 | 定位 Timeseries Metadata |
| Timeseries Metadata | 一个逻辑序列 | 选择 Chunk 偏移及序列 Statistics |
| Chunk Metadata | 一个物理 Chunk | 跳转到数据并利用 Chunk Statistics 裁剪 |
| Page Header | 一个 Page | 利用 Page Statistics 裁剪并读取载荷 |
统计信息
Page、Chunk 和时间序列级 Statistics 使用相同的类型相关结构:
Statistics :=
UVarInt count
int64 start_time
int64 end_time
... type-specific fieldscount 是该结构覆盖的值数。非对齐序列的值数等于点数;对齐值列的 count 是非 null 值数;对齐时间列的 count 是时间戳数。
类型专有字段按以下顺序紧随公共前缀:
| 数据类型 | 类型专有字段 |
|---|---|
BOOLEAN | bool first, bool last, int64 true_count |
INT32, DATE | int32 min, int32 max, int32 first, int32 last, int64 sum |
INT64, TIMESTAMP | int64 min, int64 max, int64 first, int64 last, float64 sum |
FLOAT | float32 min, float32 max, float32 first, float32 last, float64 sum |
DOUBLE | float64 min, float64 max, float64 first, float64 last, float64 sum |
TEXT | Binary32 first, Binary32 last |
STRING | Binary32 first, Binary32 last, Binary32 min, Binary32 max |
BLOB, OBJECT, VECTOR | 无类型专有字段 |
Statistics 自身不携带数据类型编号。解析 Statistics 时,数据类型来自 Chunk Header 或 Timeseries Metadata。
first 和 last 表示值的先后顺序,不等同于最小值和最大值。对于按时间有序的序列,它们对应覆盖区间首尾时间戳处的值。start_time 和 end_time 均包含在统计区间内。数值 sum 用于聚合;其中 INT64 的 sum 以 float64 保存,数值较大时可能无法精确表示每个整数值。
统计信息用于排除不可能匹配的数据。例如,Page 的时间范围完全早于查询范围时,可以跳过该 Page。范围有重叠只说明“可能匹配”,仍需读取数据检查具体值。 BLOB、OBJECT、TEXT 等没有最小/最大值字段的类型不能通过 Statistics 做值范围裁剪。
Chunk Statistics 汇总该 Chunk 的所有 Page,序列级 Statistics 再汇总该序列的所有 Chunk。父层计数是各子层计数之和,时间范围也应覆盖各子层的时间范围。
时间序列元数据
每条时间序列对应一个 TimeseriesMetadata:
TimeseriesMetadata :=
byte timeseries_type
VarString measurement_id
byte data_type
UVarInt chunk_metadata_list_size
Statistics series_statistics
byte chunk_metadata_list[chunk_metadata_list_size]timeseries_type 的高位表示列角色,低 6 位表示 Chunk Metadata 的布局:
bit 7 = 1: aligned time column
bit 6 = 1: aligned value column
low 6 bits = 0: exactly one Chunk; ChunkMetadata omits Statistics
low 6 bits = 1: more than one Chunk; every ChunkMetadata contains StatisticsChunk 元数据
当 (timeseries_type & 0x3f) == 0 时,时间序列只有一个 Chunk:
ChunkMetadata := int64 offset_of_chunk_header该 Chunk 的 Statistics 直接使用 series_statistics。
当 (timeseries_type & 0x3f) != 0 时:
ChunkMetadata :=
int64 offset_of_chunk_header
Statistics chunk_statisticsoffset_of_chunk_header 指向 Chunk marker,即 Chunk Header 的第一个字节。
chunk_metadata_list_size 保存列表的序列化字节数,不是 Chunk 数量。将它加到列表起点即可得到结束位置,然后逐条读取 Chunk Metadata,直到恰好到达该位置。
元数据索引
MetadataIndexNode :=
UVarInt child_count
MetadataEntry child[child_count]
int64 end_offset
byte node_typenode_type 决定节点层级、Entry 的编码和 Entry 指向的数据:
| 值 | 名称 | Entry 编码 | Entry 指向 |
|---|---|---|---|
| 0 | INTERNAL_DEVICE | DeviceIndexEntry | 子设备索引节点 |
| 1 | LEAF_DEVICE | DeviceIndexEntry | 设备的测量索引根节点 |
| 2 | INTERNAL_MEASUREMENT | MeasurementIndexEntry | 子测量索引节点 |
| 3 | LEAF_MEASUREMENT | MeasurementIndexEntry | 一段 Timeseries Metadata |
Entry 格式为:
DeviceIndexEntry :=
DeviceID first_device_id
int64 offset
MeasurementIndexEntry :=
VarString first_measurement_id
int64 offset每个 Entry 描述半开字节区间 [offset, next_offset)。next_offset 取下一个 Entry 的偏移量;最后一个 Entry 则取节点的 end_offset。结束偏移处的字节属于下一个区间。
entry[0].offset <= entry[1].offset <= ... <= end_offsetEntry 的 key 是其覆盖范围内的第一个 key。设备索引按 DeviceID 片段逐段排序,null 小于非 null;短元组在公共前缀相同时排在长元组之前。测量索引按测量名词典序排序。
索引查找
在索引节点中查找键 K 时,选择 first key 小于或等于 K 的最后一个 Entry。所选字节区间从该 Entry 的 offset 开始,在下一 Entry 的 offset 处结束;最后一个 Entry 则在节点 end_offset 处结束。内部节点区间包含下一级索引节点,叶子区间包含上表中对应的数据对象。
如果没有 Entry 的键小于或等于 K,该节点中就不存在目标键。测量索引的一个叶子区间可以包含多条 Timeseries Metadata,需要在该区间中继续查找完整的测量名。
沿索引继续读取前,检查:
child_count与节点内完整 Entry 数量一致。- Entry key 按该节点的比较规则有序。
- Entry offset 单调不减,每个引用区间长度非负。
- 最后一个 Entry offset 不超过
end_offset。 - 子节点层级与父节点的 node type 相符。
first-key 表示允许实现在节点内部对 Entry 使用二分查找,但文件格式不规定具体内存搜索算法。
文件元数据
TsFileMetadata 位于文件尾部长度字段之前:
TsFileMetadata :=
UVarInt table_index_count
TableIndexRoot table_index[table_index_count]
UVarInt table_schema_count
NamedTableSchema table_schema[table_schema_count]
int64 meta_offset
BloomFilter bloom_filter
VarInt property_count
Property property[property_count]
TableIndexRoot :=
VarString table_name
MetadataIndexNode device_index_root
NamedTableSchema :=
VarString table_name
TableSchema schema
Property :=
VarString key
VarBinary value属性键是 UTF-8 字符串,值是可空的原始字节数组,不要求为 UTF-8。例如,值字节 ff fe 00 80 序列化为 08 ff fe 00 80,其中 08 是 VarInt(4)。null 值为 01(VarInt(-1)),空值为 00。未知属性值必须逐字节保留,仅在具体属性约定要求时才按文本解码。
meta_offset 指向数据区末尾的 0x02 separator。TsFileMetadata 前没有 marker,其起始位置由文件尾保存的 4 字节长度确定。
Map 项的物理顺序不参与文件语义。table_index 保存树模型和表模型的设备索引根; table_schema 保存显式表模型的 schema。
文件尾四字节元数据长度位于 TsFileMetadata 之外。设文件长度为 F、元数据长度为 L,读取器使用下式得到精确字节区间:
file_metadata_start = F - 6 - 4 - L
file_metadata_end = F - 6 - 4 # 半开区间结束位置读取器必须恰好消费 L 字节。该检查可以在接受尾部魔数之前发现 Schema 不匹配或变长字段损坏。
表 Schema
TableSchema :=
UVarInt column_count
TableColumn column[column_count]
TableColumn :=
MeasurementSchema schema
int32 column_category
MeasurementSchema :=
String32 column_name
byte data_type
byte encoding_type
byte compression_type
int32 property_count
SchemaProperty property[property_count]
SchemaProperty :=
String32 key
String32 valueMeasurementSchema 使用 String32,而不是 Footer 其他位置常用的 VarString。
列类别编号为:
| 值 | 类别 |
|---|---|
| 0 | TAG |
| 1 | FIELD |
| 2 | ATTRIBUTE |
| 3 | TIME |
Schema 保留列顺序,其中 TAG 列的顺序决定 DeviceID 片段的顺序。FIELD Chunk 通过 measurement_id 标识测点列,其物理排列顺序不必与 Schema 中 FIELD 列的顺序相同。
TAG 列标识设备,FIELD 列保存测量值,ATTRIBUTE 列描述实体,TIME 标识时间列。列类别说明这一列的用途,data_type 说明它保存哪种值,两者共同决定列的含义。