TDengine 数据文件格式 — TSDB 文件集的物理结构与块编码

分类 :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: 为什么不用单一大文件存所有数据?

按时间分文件的优势:

  1. 过期删除 = 删文件(O(1),无需扫描)
  2. 查询时间范围明确时可跳过无关文件
  3. 多级存储可按文件粒度迁移

Q2: .head 和 .data 为什么分开存储?

分离索引和数据允许:

  • 查询时先只读索引(小文件,可缓存)
  • 确定需要哪些块后才读数据文件
  • 避免大数据文件的全量扫描

Q3: 列式存储对 SELECT * 有影响吗?

SELECT * 需要读取所有列,列式存储下需要拼装每一列。但由于时序查询大多只关注少数列(如 SELECT avg(temperature)),列式存储的列裁剪优势远大于全列读取的劣势。

参考

系统构架篇

数据模型

存储引擎

关于 TDengine

TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。

相关推荐
JGDT_14 小时前
直播回顾5|前沿洞察:自主智能体与垂直模型引领财务技术演进
大数据·人工智能
涛思数据(TDengine)14 小时前
牵手西门子 Xcelerator,TDengine 加速进入全球工业数字化生态
大数据·时序数据库·tdengine·国产数据库·工业数据库
热爱Liunx的丘丘人14 小时前
搭建一个 Web + 数据库系统(Nginx+PHP+MySQL)
数据库·nginx·php
战族狼魂14 小时前
Powabase 新手快速入门与实战指南
数据库
whn197714 小时前
达梦数据文件的移动或改名
数据库
cfm_291414 小时前
了解Redis
数据库·redis·缓存
KaMeidebaby14 小时前
卡梅德生物技术快报|斑点杂交 + 膜芯片:6 种水果源性成分检测技术实操拆解
前端·人工智能·物联网·其他·百度·新浪微博
计算机安禾14 小时前
【算法分析与设计】第18篇:改进的最大流算法:Edmonds-Karp与Dinic
大数据·人工智能·算法
2603_9547083114 小时前
边缘计算在微电网架构中的应用:低时延控制的技术支撑
人工智能·物联网·架构·能源·边缘计算