分类 :3.存储引擎 | 篇章:04 数据文件格式

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-05-28
TSDB 数据文件是 TDengine 时序数据的最终持久化形式。数据按时间范围组织为多个文件集(File Set),每个文件集包含索引文件、数据文件、预聚合文件等,内部采用列式存储和分块压缩,实现高压缩率和快速列裁剪查询。
核心概念速查表
| 概念 | 说明 |
|---|---|
| File Set | 覆盖一个 DURATION 时间段的一组文件 |
| fid | File Set 的编号,由时间戳计算得出 |
| .head 文件 | 数据块索引(Block Index),记录每个块的位置和统计 |
| .data 文件 | 列数据存储,按块分组、按列压缩 |
| .sma 文件 | 块级预聚合数据(SUM/MIN/MAX/COUNT) |
| .stt 文件 | Sorted String Table,未合并的落盘数据 |
| .tomb 文件 | 删除记录(tombstone),标记已删除的时间范围 |
| Data Block | 数据块,包含一张子表一段时间的数据 |
详细解析
1. 文件集组织
TSDB 文件集按时间分组:
tsdb/
├── current ← 元信息(文件集列表)
│
├── v<fid0>f<commitId>.head ← FileSet 0(时间段 T0~T1)
├── v<fid0>f<commitId>.data
├── v<fid0>f<commitId>.sma
├── v<fid0>f<commitId>.stt
│
├── v<fid1>f<commitId>.head ← FileSet 1(时间段 T1~T2)
├── v<fid1>f<commitId>.data
├── v<fid1>f<commitId>.sma
├── v<fid1>f<commitId>.stt
│
└── ...
fid 计算:fid = (timestamp - 数据库起始时间) / DURATION
commitId:每次 Commit 递增,用于区分同一 fid 的不同版本
2. .head 文件(块索引)
.head 文件存储所有数据块的索引信息,查询时先读 .head 确定需要读取的数据块位置:
.head 文件结构:
┌─────────────────────────────────────────┐
│ .head 文件 │
│ │
│ ┌───────────────────────────────────┐ │
│ │ File Header │ │
│ │ - magic number │ │
│ │ - 文件版本 │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ Block Index(按 uid 排序) │ │
│ │ │ │
│ │ uid=1001: │ │
│ │ Block 0: offset, size, minTs, │ │
│ │ maxTs, numRows, minVer │ │
│ │ Block 1: ... │ │
│ │ │ │
│ │ uid=1002: │ │
│ │ Block 0: ... │ │
│ │ Block 1: ... │ │
│ │ │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ Block Index 的索引(二级索引) │ │
│ │ 快速定位某个 uid 的块索引位置 │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
3. .data 文件(列数据)
.data 文件存储实际的列式数据,按数据块(Block)组织:
.data 文件结构:
┌─────────────────────────────────────────────────────┐
│ .data 文件 │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Data Block(一个数据块) │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ Block Header (SDiskDataHdr) │ │ │
│ │ │ - delimiter: 0xF00AFA0F(魔数) │ │ │
│ │ │ - uid: 子表 UID │ │ │
│ │ │ - suid: 超级表 UID │ │ │
│ │ │ - nRows: 行数 │ │ │
│ │ │ - numOfCols: 列数 │ │ │
│ │ │ - szBlkCol: 列元信息数组大小 │ │ │
│ │ │ - szVersion: 版本号数组大小 │ │ │
│ │ │ - szKey: 时间戳数组大小 │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ Column Metadata (SBlockCol[]) │ │ │
│ │ │ 每列一个: │ │ │
│ │ │ - colId: 列编号 │ │ │
│ │ │ - type: 数据类型 │ │ │
│ │ │ - flag: 是否全 NULL / 是否有 NULL │ │ │
│ │ │ - szBitmap: NULL 位图大小 │ │ │
│ │ │ - szOffset: 变长列偏移数组大小 │ │ │
│ │ │ - szValue: 压缩后值数组大小 │ │ │
│ │ │ - offset: 列数据在块内的偏移 │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ Version Array(版本号数组) │ │ │
│ │ │ 每行对应一个版本号(用于 MVCC) │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ Timestamp Column(时间戳列) │ │ │
│ │ │ Delta-of-Delta 编码 + 压缩 │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ Column 1 Data │ │ │
│ │ │ [NULL Bitmap] + [Values(编码+压缩)] │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ Column 2 Data │ │ │
│ │ │ [NULL Bitmap] + [Offsets] + [Values] │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ ... │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Data Block (next block...) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
4. 数据块内的列布局
单列在数据块中的存储格式:
定长列(如 INT、FLOAT):
┌──────────────┬─────────────────────────────┐
│ NULL Bitmap │ Values(类型特化编码+LZ4压缩)│
│ ⌈N/8⌉ 字节 │ 压缩后的值数组 │
└──────────────┴─────────────────────────────┘
变长列(如 BINARY、NCHAR):
┌──────────────┬──────────────┬──────────────────┐
│ NULL Bitmap │ Offset Array │ Values(连续存放)│
│ ⌈N/8⌉ 字节 │ N × 4 字节 │ 压缩后的字节流 │
└──────────────┴──────────────┴──────────────────┘
Offset Array: 每行在 Values 区的起始偏移
用于随机访问某行的变长值
5. .sma 文件(预聚合)
.sma 文件结构:
为每个数据块存储预计算的聚合值:
Block 对应的 SMA 记录:
┌─────────────────────────────────────┐
│ Column 0 (timestamp): │
│ min, max │
│ Column 1 (numeric): │
│ sum, min, max, numOfNull │
│ Column 2 (numeric): │
│ sum, min, max, numOfNull │
│ ... │
└─────────────────────────────────────┘
用途:
- SELECT COUNT(*) → 直接用 nRows,无需读数据
- SELECT MIN(col) WHERE ts > X → 先比较块级 min/max
如果 max < 查询条件值 → 整块跳过
- 大幅减少不必要的数据块读取
6. .stt 文件(Sorted String Table)
STT 文件存储尚未合并到主数据文件的落盘数据:
.stt 文件特点:
- 内部结构与 .data 类似(块索引 + 数据块)
- 可能包含多张子表的混合数据
- 数据在块内有序,但不同块间可能时间重叠
- 多个 STT 文件可以共存(STT_TRIGGER 控制)
STT 文件的生命周期:
MemTable flush → 写入 STT 文件
STT 文件数达到 STT_TRIGGER → 触发合并
合并后 → STT 数据整合到 .head/.data/.sma
旧 STT 文件删除
7. .tomb 文件(删除标记)
.tomb 文件结构:
记录 DELETE 操作的时间范围:
┌─────────────────────────────────────────┐
│ Delete Record 1: │
│ uid: 目标子表 │
│ startTs: 删除起始时间 │
│ endTs: 删除结束时间 │
│ version: 删除操作的版本号 │
├─────────────────────────────────────────┤
│ Delete Record 2: ... │
└─────────────────────────────────────────┘
查询时:读取 .tomb → 过滤掉被删除的时间范围
合并时:物理删除被标记的数据行
8. 查询时的文件读取路径
查询一张子表的数据:
SELECT * FROM d1001 WHERE ts >= T1 AND ts <= T2
│
▼
① 计算 fid 范围:
fid_start = (T1 - dbStartTime) / DURATION
fid_end = (T2 - dbStartTime) / DURATION
│
▼
② 对每个相关 FileSet:
a. 读取 .head → 找到 uid=d1001 的块索引
b. 遍历块索引,按 [minTs, maxTs] 过滤:
- maxTs < T1 → 跳过
- minTs > T2 → 跳过
- 有交集 → 需要读取
c. 读取 .sma → 检查块级统计是否满足其他条件
d. 读取 .data → 定位到块偏移 → 解压数据块
e. 读取 .tomb → 过滤已删除行
│
▼
③ 合并 MemTable + STT + .data 的结果
│
▼
④ 输出有序结果
9. 多路合并读取算法
查询时需要将来自 MemTable、STT 文件和主数据文件的数据合并为统一有序结果。以下是具体算法:
多路归并排序算法(针对单子表 uid):
数据源(每个数据源内部已按时间戳有序):
┌─────────────────┐ ┌─────────────────┐
│ Active MemTable │ │ Immutable MemTbl│
│ 数据源 1 │ │ 数据源 2 │
└────────┬────────┘ └────────┬────────┘
│ │
┌────────┴────────┐ ┌───────┴─────────┐
│ STT File 1 │ │ STT File 2 │
│ 数据源 3 │ │ 数据源 4 │
└────────┬────────┘ └────────┬────────┘
│ │
┌────────┴──────────────────────┴────────┐
│ .data 主文件(有序块序列) │
│ 数据源 5 │
└────────────────────┬───────────────────┘
│
▼
┌───────────────────────┐
│ 优先队列(Min-Heap) │
│ │
│ 按 (timestamp, ver) │
│ 排序的堆顶即为下一行 │
└───────────┬───────────┘
│
▼
输出有序结果行
合并排序的详细规则:
排序键: (timestamp ASC, version DESC)
① 初始化:从每个数据源取第一行,放入优先队列
② 循环弹出堆顶(timestamp 最小的行):
- 如果堆顶 timestamp 在 .tomb 删除范围内 → 跳过
- 如果堆顶 timestamp == 上一输出行的 timestamp:
→ 同一时间戳有多个版本
→ 只取 version 最高的那行(最新写入胜出)
→ 丢弃其他相同时间戳的行
- 否则 → 输出该行到结果集
③ 从弹出行的数据源取下一行,补入优先队列
④ 重复 ②③ 直到所有数据源耗尽
版本号(version)的作用:
每次写入操作分配一个全局递增的 version
场景:设备补报历史数据
原始写入: ts=T1, ver=100, value=25.0
补报覆写: ts=T1, ver=500, value=25.3
合并时: 两者 timestamp 相同
→ 取 ver=500 的 25.3(后写入的覆盖先写入的)
→ ver=100 的 25.0 被丢弃
version 保证:
- .data 文件中: 块头记录 minVer/maxVer
- MemTable 中: 每行携带 version
- STT 中: 块内按 (ts, ver) 有序
→ 多路合并可以正确处理任何写入顺序
优化策略:
① 数据源裁剪:
如果某个 STT 文件的 [minTs, maxTs] 与查询无交集
→ 直接跳过,不参与合并
② 单源快速路径:
如果只有一个数据源有数据(常见情况:无乱序)
→ 跳过多路合并,直接顺序输出
③ 块级版本检查:
.data 块的 maxVer < STT 块的 minVer
→ 说明 STT 数据全部更新
→ .data 块在重叠时间段内可整块跳过
④ 非重叠时间段的拼接:
.data: [T1~T5], STT: [T6~T10]
时间段不重叠 → 无需归并排序
→ 按时间顺序拼接即可(避免堆操作开销)
9. 数据块大小控制
| 参数 | 默认值 | 作用 |
|---|---|---|
| MAXROWS | 4096 | 单块最大行数,达到即开新块 |
| MINROWS | 100 | 单块最小行数(影响落盘时机) |
| TSDB_PAGESIZE | 4 KB | I/O 对齐页大小 |
数据块大小估算:
行宽 100 字节 × 4096 行 = 400 KB(压缩前)
COMP=2 压缩后 ≈ 40~80 KB
块越大:
✓ 压缩率越高(更多相似数据)
✓ 减少块索引条目数
✗ 点查需要解压更多无用数据
块越小:
✓ 点查更快(解压数据量小)
✗ 压缩率降低
✗ 块索引膨胀
代码示例
查看文件集状态
sql
-- 通过系统表查看数据文件信息
SHOW power.VGROUPS;
-- 查看磁盘使用
SHOW CLUSTER VARIABLES;
bash
# 直接观察磁盘文件
ls -la /var/lib/taos/vnode/vnode2/tsdb/
# v0f1.head v0f1.data v0f1.sma v0f1.stt v1f1.head ...
性能考量
文件数量对查询的影响
| 因素 | 影响 |
|---|---|
| FileSet 数量多 | 时间定位精确,减少不必要文件读取 |
| 单 FileSet 内块多 | 需要读更多 .head 索引数据 |
| STT 文件多 | 查询需要多路合并,延迟增加 |
| .tomb 记录多 | 每次查询需要检查删除标记 |
I/O 模式
| 操作 | I/O 模式 | 优化 |
|---|---|---|
| 写入 WAL | 顺序追加 | 高吞吐 |
| 写入 .data | 顺序写新文件 | 无随机写 |
| 读取 .head | 随机读(定位块) | 尽量缓存 |
| 读取 .data | 顺序读(整块) | 预读优化 |
FAQ
Q1: 为什么不用单一大文件存所有数据?
按时间分文件的优势:
- 过期删除 = 删文件(O(1),无需扫描)
- 查询时间范围明确时可跳过无关文件
- 多级存储可按文件粒度迁移
Q2: .head 和 .data 为什么分开存储?
分离索引和数据允许:
- 查询时先只读索引(小文件,可缓存)
- 确定需要哪些块后才读数据文件
- 避免大数据文件的全量扫描
Q3: 列式存储对 SELECT * 有影响吗?
SELECT * 需要读取所有列,列式存储下需要拼装每一列。但由于时序查询大多只关注少数列(如 SELECT avg(temperature)),列式存储的列裁剪优势远大于全列读取的劣势。
参考
系统构架篇
- 01-《TDengine 整体架构全景》
- 02-《集群拓扑深度解析》
- 03-《MNode 内部机制深度解析》
- 04-《RPC 通信层深度解析》
- 05-《VNode 生命周期》
- 06-《RAFT 共识协议》
- 07-《端到端的消息流》
数据模型
- 01-《数据库创建与参数详解》
- 02-《超级表/子表/普通表》
- 03-《支持数据类型深度解析》
- 04-《TDengine Tag 设计哲学与 Schema 变更机制》
- 05-《TDengine 虚拟表实现原理》
存储引擎
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。