blob: a5219659498903c055e69883e637ca793c0b8439 [file] [view]
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# 元数据
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 裁剪并读取载荷 |
<div id="statistics"></div>
## 统计信息
Page、Chunk 和时间序列级 Statistics 使用相同的类型相关结构:
```text
Statistics :=
UVarInt count
int64 start_time
int64 end_time
... type-specific fields
```
`count` 是该结构覆盖的值数。非对齐序列的值数等于点数;对齐值列的 `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。父层计数是各子层计数之和,时间范围也应覆盖各子层的时间范围。
<div id="timeseries-metadata"></div>
## 时间序列元数据
每条时间序列对应一个 `TimeseriesMetadata`:
```text
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 的布局:
```text
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 Statistics
```
<div id="chunk-metadata"></div>
## Chunk 元数据
当 `(timeseries_type & 0x3f) == 0` 时,时间序列只有一个 Chunk:
```text
ChunkMetadata := int64 offset_of_chunk_header
```
该 Chunk 的 Statistics 直接使用 `series_statistics`。
当 `(timeseries_type & 0x3f) != 0` 时:
```text
ChunkMetadata :=
int64 offset_of_chunk_header
Statistics chunk_statistics
```
`offset_of_chunk_header` 指向 Chunk marker,即 Chunk Header 的第一个字节。
`chunk_metadata_list_size` 保存列表的序列化字节数,不是 Chunk 数量。将它加到列表起点
即可得到结束位置,然后逐条读取 Chunk Metadata,直到恰好到达该位置。
<div id="metadata-index"></div>
## 元数据索引
```text
MetadataIndexNode :=
UVarInt child_count
MetadataEntry child[child_count]
int64 end_offset
byte node_type
```
`node_type` 决定节点层级、Entry 的编码和 Entry 指向的数据:
| 值 | 名称 | Entry 编码 | Entry 指向 |
| ---: | --- | --- | --- |
| 0 | `INTERNAL_DEVICE` | `DeviceIndexEntry` | 子设备索引节点 |
| 1 | `LEAF_DEVICE` | `DeviceIndexEntry` | 设备的测量索引根节点 |
| 2 | `INTERNAL_MEASUREMENT` | `MeasurementIndexEntry` | 子测量索引节点 |
| 3 | `LEAF_MEASUREMENT` | `MeasurementIndexEntry` | 一段 Timeseries Metadata |
Entry 格式为:
```text
DeviceIndexEntry :=
DeviceID first_device_id
int64 offset
MeasurementIndexEntry :=
VarString first_measurement_id
int64 offset
```
每个 Entry 描述半开字节区间 `[offset, next_offset)`。`next_offset` 取下一个 Entry 的
偏移量;最后一个 Entry 则取节点的 `end_offset`。结束偏移处的字节属于下一个区间。
```text
entry[0].offset <= entry[1].offset <= ... <= end_offset
```
Entry 的 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 使用二分查找,但文件格式不规定具体内存搜索
算法。
<div id="file-metadata"></div>
## 文件元数据
`TsFileMetadata` 位于文件尾部长度字段之前:
```text
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`,
读取器使用下式得到精确字节区间:
```text
file_metadata_start = F - 6 - 4 - L
file_metadata_end = F - 6 - 4 # 半开区间结束位置
```
读取器必须恰好消费 `L` 字节。该检查可以在接受尾部魔数之前发现 Schema 不匹配或变长字段
损坏。
<div id="table-schema"></div>
## 表 Schema
```text
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 value
```
`MeasurementSchema` 使用 `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` 说明它保存哪种值,两者共同决定列的含义。