
TDengine 存储引擎设计文档
一、设计理念
TDengine 存储引擎专为时序数据场景设计,核心理念是:充分利用时序数据的时间有序性、连续性和高并发特点,实现极致的写入速度和超高的压缩比。
核心设计目标
- 极速写入:顺序写入优化,支持百万级 TPS
- 超高压缩:10:1 甚至更高的压缩比
- 快速查询:无需索引快速定位数据
- 海量并发:支持亿级表规模
二、存储架构总览
┌─────────────────────────────────────────────────┐
│ vnode 存储单元 │
├─────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ WAL │ │ META │ │ TSDB │ │
│ │ 预写日志 │ │ 元数据 │ │ 时序数据 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ↓ ↓ ↓ │
│ 持久化保证 B+Tree存储 LSM存储引擎 │
└─────────────────────────────────────────────────┘
三、核心技术特性
3.1 一表一采集点模型 - 速度快的根本
设计理念:一个数据采集点(如一个传感器)对应一张表
sql
-- 创建超级表(模板)
CREATE STABLE sensors (
ts TIMESTAMP, -- 时间戳
temperature FLOAT, -- 温度
humidity INT -- 湿度
) TAGS (location BINARY(64), device_id INT);
-- 每个传感器是一张子表
CREATE TABLE sensor_001 USING sensors TAGS ('北京机房', 1);
CREATE TABLE sensor_002 USING sensors TAGS ('上海机房', 2);
为什么这么快?
-
顺序写入优化
传统数据库(一个表存所有设备):
Device_1: [t1,v1] → 随机位置
Device_2: [t1,v2] → 随机位置
Device_3: [t1,v3] → 随机位置
↓ 磁盘随机写入,速度慢TDengine(每个设备一张表):
Sensor_001表: [t1,v1] → [t2,v2] → [t3,v3] → ...
↓ 顺序追加写入,速度快 10x+ -
查询性能优化
-- 查询单个传感器数据,一次连续读取
SELECT * FROM sensor_001
WHERE ts >= '2024-01-01' AND ts < '2024-01-02';-- 内部执行:直接定位 sensor_001 的数据块,连续读取
-- 无需扫描其他传感器数据 -
标签与数据分离存储
元数据层(META):
sensor_001 → tags: {location:'北京机房', device_id:1}数据层(TSDB):
sensor_001 → [时间列][温度列][湿度列]优势:
✓ 标签不重复存储,节省空间 90%+
✓ 查询先过滤标签,再读数据,速度快
3.2 LSM 存储结构 - 写入快,不阻塞
为什么不用 B+Tree?
B+Tree 问题:
数据量增长 → 树高度增加 → 查询性能下降
[10GB数据: 3层] → [100GB: 4层] → [1TB: 5层] ✗
LSM 优势:
写入量增长 → 合并优化 → 查询性能稳定
始终保持高性能 ✓
LSM 写入流程(快速且可靠):
第1步:写入内存(毫秒级)
数据 → MemTable (Red-Black Tree + SkipList)
↓ 快速索引,立即返回成功
第2步:写入 WAL(保证持久性)
并行写入 WAL 预写日志
↓ 断电可恢复,数据不丢失
第3步:内存满触发落盘(后台进行)
MemTable → STT 文件(小文件)
↓ 不阻塞写入
第4步:后台合并(优化查询)
多个 STT → 合并成 Data 文件(大文件)
↓ 减少文件数量,提升查询速度
双索引设计:
c
// Red-Black Tree: 索引不同表
RBTree {
sensor_001 → SkipList_1
sensor_002 → SkipList_2
sensor_003 → SkipList_3
...
}
// SkipList: 索引单表数据(按时间排序)
SkipList_1 {
Level 3: [t1] ----------------→ [t100]
Level 2: [t1] ----→ [t50] ----→ [t100]
Level 1: [t1] → [t25] → [t50] → [t75] → [t100]
Level 0: [t1] → [t2] → ... → [t99] → [t100]
}
查询时间复杂度:O(log n)
3.3 时间分区 - 无索引快速定位
设计思想:按时间段分文件存储
/var/lib/taos/vnode2/tsdb/
├── v2f1845.data ← 2024-01-01 ~ 2024-01-10
├── v2f1845.head
├── v2f1846.data ← 2024-01-11 ~ 2024-01-20
├── v2f1846.head
├── v2f1847.data ← 2024-01-21 ~ 2024-01-30
└── v2f1847.head
时间段长度由 duration 参数控制
查询加速原理:
sql
-- 查询 2024-01-15 的数据
SELECT * FROM sensors WHERE ts = '2024-01-15';
-- 执行过程:
1. 计算文件组编号:(2024-01-15 - 起始时间) / duration = 1846
2. 直接打开 v2f1846.data 文件
3. 无需扫描其他文件
速度:O(1) 时间复杂度,毫秒级定位!
数据生命周期管理:
sql
-- 创建数据库,设置保留策略
CREATE DATABASE sensor_db
DURATION 10d -- 每个文件存 10 天数据
KEEP 365d; -- 保留 365 天
-- 系统自动操作:
超过 365 天的文件 → 自动删除 → 释放空间
3.4 列式存储 + 多级压缩 - 压缩比高的秘密
列式存储优势
行式存储(传统数据库):
[t1,temp1,hum1][t2,temp2,hum2][t3,temp3,hum3]
↓ 每列数据类型不同,压缩效率低
列式存储(TDengine):
时间列: [t1, t2, t3, ..., tn] ← 时间递增,规律性强
温度列: [25.1, 25.2, 25.1, ...] ← 数值相近,波动小
湿度列: [60, 61, 60, ...] ← 数值稳定
↓ 同列数据特征相似,压缩效率高 10x+
一级压缩:针对数据类型优化
整数型压缩:
c
原始数据: [1000, 1001, 1002, 1003, 1004]
Delta 编码:
[1000, +1, +1, +1, +1] // 存储差值
Delta-Delta 编码:
[1000, +1, 0, 0, 0] // 存储差值的差值
↓ 大量 0 值
Simple8B 编码:
将多个小数字打包到一个 64bit 整数
↓ 压缩比 10:1+
浮点型压缩:
c
温度数据: [25.1, 25.2, 25.1, 25.3, 25.2]
Delta-Delta 编码:
基准值: 25.1
差值: [0, +0.1, 0, +0.2, +0.1]
差值的差值: [0, +0.1, -0.1, +0.2, -0.1]
↓ 数据变化规律明显,压缩效果好
时间戳压缩:
c
时间戳: [1704067200, 1704067260, 1704067320, 1704067380]
(2024-01-01 00:00:00, 每隔60秒)
Delta 编码:
[1704067200, +60, +60, +60]
↓ 等间隔采样,大量重复差值
最终存储: [起始时间][间隔60秒][记录数4]
↓ 压缩比 16:1 (16字节 → 1字节)
字符串压缩:
c
设备类型: ["sensor", "sensor", "sensor", "actuator", "sensor"]
字典压缩:
字典: {0: "sensor", 1: "actuator"}
编码: [0, 0, 0, 1, 0]
↓ 长字符串变成短整数
二级压缩:通用算法进一步压缩
一级压缩后的数据
↓ 仍有规律可循
LZ4 压缩(速度快):
查找重复模式,用引用替代
压缩比: 2-3x
速度: 500 MB/s
ZSTD 压缩(压缩比高):
更智能的匹配算法
压缩比: 3-5x
速度: 300 MB/s
ZLIB 压缩(平衡):
压缩比: 2.5-4x
速度: 400 MB/s
有损压缩(可选)
c
// 浮点数精度控制
原始数据: 25.123456789
设置精度: 0.01
存储数据: 25.12
温度传感器精度通常 0.1°C,存储 0.001°C 没有意义
↓ 有损压缩可额外节省 30-50% 空间
压缩效果对比:
原始数据: 1000 MB
一级压缩后: 100 MB (压缩比 10:1)
└─ Delta-Delta + Simple8B
二级压缩后: 30 MB (最终压缩比 33:1)
└─ ZSTD 压缩
实际案例:
- 稳定工况数据:压缩比可达 50:1
- 一般时序数据:压缩比 10:1 ~ 20:1
3.5 BRIN 索引 - 快速过滤数据块
设计思想:存储数据块的统计信息,而非每条记录的索引
传统索引(B+Tree):
├─ 记录1 → 位置1
├─ 记录2 → 位置2
├─ ...
└─ 记录100万 → 位置100万
↓ 索引巨大,占用大量空间
BRIN 索引(块范围索引):
├─ 数据块1: [时间范围: t1~t1000, 最大值, 最小值, 总和]
├─ 数据块2: [时间范围: t1001~t2000, 最大值, 最小值, 总和]
└─ 数据块N: [时间范围: ..., 最大值, 最小值, 总和]
↓ 索引小,查询快
head 文件结构:
c
// v2f1845.head 文件内容
struct BRINRecord {
uint64_t tableId; // 表ID
int64_t minTimestamp; // 最小时间戳
int64_t maxTimestamp; // 最大时间戳
uint64_t offset; // 数据块在 .data 文件中的偏移
uint32_t length; // 数据块长度
// 预计算统计信息(SMA)
double minValue; // 最小值
double maxValue; // 最大值
double sum; // 总和
int32_t count; // 记录数
};
查询加速示例:
sql
-- 查询最大温度
SELECT MAX(temperature) FROM sensor_001
WHERE ts >= '2024-01-15' AND ts < '2024-01-16';
-- 执行过程:
1. 打开 v2f1845.head 文件
2. 扫描 BRIN 记录,找到时间范围匹配的数据块
3. 直接读取 BRIN 记录中的 maxValue
4. 无需读取 .data 文件中的原始数据
结果:查询速度提升 100x+
BRIN 索引本身也压缩:
BRIN 记录采用列式存储 + 压缩:
tableId列: [1001, 1001, 1001, ...] ← 大量重复
minTime列: [t1, t1000, t2000, ...] ← 递增序列
maxTime列: [t999, t1999, t2999, ...] ← 递增序列
offset列: [0, 4096, 8192, ...] ← 等差序列
经过压缩后,索引文件非常小
3.6 预计算(SMA)- 秒级聚合查询
设计思想:在数据写入时计算统计信息,查询时直接使用
sql
-- 这类查询无需读取原始数据
SELECT MAX(temperature), MIN(temperature), AVG(humidity)
FROM sensors
WHERE ts >= '2024-01-01' AND ts < '2024-02-01';
-- 执行过程:
1. 扫描 head 文件中的 BRIN 记录
2. 读取每个数据块的统计信息:
- maxValue (最大值)
- minValue (最小值)
- sum (总和)
- count (记录数)
3. 聚合计算:
- MAX = max(所有块的maxValue)
- MIN = min(所有块的minValue)
- AVG = sum(所有块的sum) / sum(所有块的count)
4. 返回结果
I/O 减少:99%+(只读索引,不读数据)
查询速度:毫秒级(原本需要几秒甚至几十秒)
3.7 写驱动缓存 - 最新数据优先
设计理念:物联网场景最关心最新数据
传统缓存(读驱动):
查询什么 → 缓存什么
↓ 冷数据占用缓存空间
TDengine 缓存(写驱动):
写入什么 → 缓存什么
↓ 最新数据始终在缓存
缓存结构:
vnode 内存分配:
┌────────────────────────────┐
│ 内存块1 (正在写入) │ ← 最新数据
├────────────────────────────┤
│ 内存块2 (准备落盘) │
├────────────────────────────┤
│ 内存块3 (后台落盘中) │
└────────────────────────────┘
当内存块1写满 1/3 时:
→ 触发内存块2落盘
→ 写入切换到新的内存块1
→ 始终保持 1/3 内存缓存最新数据
查询加速:
sql
-- 查询最新记录(last_row)
SELECT last_row(*) FROM sensor_001;
-- 执行:直接从内存缓存返回,微秒级响应
-- LRU 缓存配置
CREATE DATABASE sensor_db
CACHEMODEL 'both'; -- 开启 last 和 last_row 缓存
-- 查询最新温度
SELECT last(temperature) FROM sensor_001;
↓ 命中缓存,无需访问磁盘
3.8 多表低频优化 - 碎片合并
场景:成千上万张表,每张表写入频率低
问题:
sensor_001 写入 10 条 → 落盘生成小文件
sensor_002 写入 8 条 → 落盘生成小文件
sensor_003 写入 12 条 → 落盘生成小文件
...
↓ 产生大量小文件(碎片化)
优化方案(多 STT 文件):
1. 配置多个 STT 文件(如 6 个)
2. 首次落盘 → stt1 文件
3. 再次落盘 → stt2 文件
4. ...
5. 后台合并:stt1 + stt2 + ... → data 文件
结果:
✓ 减少文件数量 90%+
✓ 提升查询性能 10x+
四、文件组织结构
完整的数据文件组
文件组 v2f1845/ (存储 2024-01-01 ~ 2024-01-10 数据)
├── v2f1845.head ← BRIN 索引文件
├── v2f1845.data ← 时序数据文件(列式存储+压缩)
├── v2f1845.sma ← 预计算文件(统计信息)
├── v2f1845.tomb ← 删除记录文件
├── v2f1845.stt1 ← 碎片数据文件1
├── v2f1845.stt2 ← 碎片数据文件2
└── ...
文件交互流程
写入流程:
数据 → MemTable → STT文件 → (后台合并) → Data文件
查询流程:
查询条件 → Head文件(BRIN索引) → 定位数据块 → Data文件
聚合查询:
查询条件 → Head文件(读取SMA) → 直接返回(无需读Data)
五、性能优势总结
5.1 写入性能
| 优化技术 | 性能提升 | 原理 |
|---|---|---|
| 顺序写入 | 10x+ | 单表数据连续存储,磁盘顺序I/O |
| 内存缓存 | 100x+ | 数据先写内存,批量落盘 |
| WAL 日志 | 2x+ | 异步刷盘,不阻塞写入 |
| LSM 结构 | 稳定 | 写入不受数据量影响 |
实测数据:
- 单机写入:100万 数据点/秒
- 集群写入:300万+ 数据点/秒
5.2 压缩比
| 数据类型 | 压缩比 | 技术 |
|---|---|---|
| 稳定工况数据 | 50:1 | Delta + ZSTD |
| 一般时序数据 | 10:1 ~ 20:1 | 列存 + 多级压缩 |
| 字符串数据 | 5:1 ~ 10:1 | 字典压缩 |
对比:
- 通用数据库:无压缩或压缩比 2:1
- TDengine:10:1 起步,最高 50:1
5.3 查询性能
| 查询类型 | 性能提升 | 原理 |
|---|---|---|
| 单表时间范围查询 | 10x+ | 时间分区,快速定位 |
| 聚合查询(MAX/MIN/AVG) | 100x+ | BRIN 索引 + SMA 预计算 |
| 最新数据查询 | 1000x+ | 写驱动缓存,内存直接返回 |
| 多表标签过滤 | 10x+ | 标签分离存储,元数据索引 |
5.4 空间占用
1亿条记录(10个字段,每条100字节)存储空间对比:
传统数据库:
原始数据: 10GB
索引: 5GB
总计: 15GB
TDengine:
原始数据: 10GB
压缩后: 0.5 - 1GB (压缩比 10:1 ~ 20:1)
BRIN索引: 10MB (极小)
总计: 0.51 - 1.01GB
空间节省:93% - 96%
六、适用场景
最佳适用场景
-
物联网设备监控
- 海量传感器数据采集
- 设备状态实时查询
- 历史数据趋势分析
-
工业互联网
- 生产线设备监控
- 能耗分析
- 预测性维护
-
车联网
- 车辆轨迹数据
- 车况实时监控
- 驾驶行为分析
-
IT 运维监控
- 服务器性能监控
- 应用日志分析
- 网络流量监控
数据特征要求
- ✅ 时间序列数据
- ✅ 写多读少
- ✅ 数据连续采集
- ✅ 需要长期存储
- ✅ 查询以时间范围为主
七、配置建议
少表高频场景
sql
CREATE DATABASE iot_db
BUFFER 256 -- 大内存缓存,提升写入性能
PAGES 64 -- 元数据缓存页数
PAGESIZE 16 -- 每页大小(KB)
DURATION 10d -- 文件组时间跨度
KEEP 365d -- 数据保留时长
MINROWS 100 -- 每块最少行数
MAXROWS 4096 -- 每块最多行数
COMP 2 -- 二级压缩
PRECISION 'ms' -- 时间精度:毫秒
SINGLE_STABLE 0; -- 多个超级表
多表低频场景
sql
CREATE DATABASE sensor_db
BUFFER 64 -- 中等内存缓存
PAGES 128 -- 更多元数据缓存(支持更多表)
PAGESIZE 8
DURATION 1d -- 更短的文件组时间跨度
KEEP 3650d -- 长期保留
MINROWS 10 -- 更小的块大小(适应低频写入)
MAXROWS 1000
COMP 2
STT_TRIGGER 6 -- 开启多 STT 文件优化
PRECISION 'ms';
八、总结
TDengine 存储引擎通过一系列创新设计,实现了时序数据库的极致性能:
速度快的核心原因
- ✅ 顺序写入:一表一采集点模型,充分利用磁盘顺序I/O
- ✅ LSM 结构:写入内存即返回,后台异步落盘
- ✅ 时间分区:无需索引快速定位数据文件
- ✅ BRIN 索引:块级索引,减少数据扫描
- ✅ 预计算:SMA 统计信息,聚合查询秒级返回
- ✅ 写驱动缓存:最新数据优先,查询命中率高
压缩比高的核心原因
- ✅ 列式存储:同列数据特征相似,压缩效率高
- ✅ 一级压缩:针对数据类型优化(Delta-Delta, Simple8B等)
- ✅ 二级压缩:通用算法进一步压缩(LZ4, ZSTD等)
- ✅ 标签分离:避免标签数据重复存储
- ✅ 有损压缩:可选的精度控制,额外节省空间
关键数据指标
- 📊 写入性能:100万+ 数据点/秒(单机)
- 📊 压缩比:10:1 ~ 50:1
- 📊 查询性能:10x ~ 1000x 提升
- 📊 空间节省:93% ~ 96%
- 📊 支持规模:亿级表数量
TDengine 存储引擎是专为时序数据场景深度优化的高性能存储方案,在保证数据可靠性的前提下,实现了写入速度、压缩比、查询性能的全面领先。
相关文章
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。