分类 :3.存储引擎 | 篇章 :08 压缩编码
适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-06-02
时序数据具有高度的规律性------时间戳单调递增、数值列缓慢变化、字符串列频繁重复。TDengine 利用这些特征设计了"一阶类型特化编码 + 二阶通用压缩"的双层压缩架构,在不牺牲查询速度的前提下实现极高的压缩比。
核心概念速查表
| 概念 | 说明 |
|---|---|
| COMP | 数据库压缩级别参数(0/1/2) |
| 一阶编码 | 类型感知的轻量编码(Delta、XOR等) |
| 二阶压缩 | 通用字节流压缩(LZ4 / ZSTD / TSZ) |
| Delta-of-Delta | 时间戳编码:存储差值的差值 |
| XOR 差分 | 浮点数编码:只存储异或后的变化位 |
| Simple8B | 整数编码:将多个小值打包到 64 位 |
| ZigZag | 有符号整数映射为无符号整数 |
详细解析
1. 双层压缩架构
TDengine 压缩的两个层次:
原始数据
│
▼
┌────────────────────────────────────────────┐
│ 一阶:类型特化编码 │
│ │
│ 根据列的数据类型选择最佳编码算法: │
│ - TIMESTAMP → Delta-of-Delta │
│ - INT/BIGINT → ZigZag + Simple8B │
│ - FLOAT/DOUBLE → XOR 差分 │
│ - BOOL → Bit Packing │
│ - BINARY/NCHAR → 字典编码 │
│ │
│ 特点:轻量、解码快、利用数据特征 │
└────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 二阶:通用压缩(COMP=2 时启用) │
│ │
│ 对一阶编码后的字节流进行通用压缩: │
│ - LZ4:高速压缩/解压,压缩率中等 │
│ - ZSTD:高压缩率,解压稍慢 │
│ - TSZ:浮点专用有损压缩(可选) │
│ │
│ 特点:进一步压缩残余冗余 │
└────────────────────────────────────────────┘
│
▼
磁盘存储
2. COMP 参数
| COMP 值 | 含义 | 行为 |
|---|---|---|
| 0 | 不压缩 | 原始数据直接写盘(调试用) |
| 1 | 一阶压缩 | 仅类型特化编码 |
| 2(默认) | 双层压缩 | 一阶编码 + 二阶通用压缩 |
sql
-- 创建数据库时指定压缩级别
CREATE DATABASE power COMP 2;
-- 修改压缩级别(影响后续写入,不影响已有数据)
ALTER DATABASE power COMP 1;
3. 时间戳编码:Delta-of-Delta
Delta-of-Delta 编码过程:
原始时间戳(等间隔采集 1 秒):
[1000, 1001, 1002, 1003, 1004, 1005]
Step 1: 计算一阶差值(Delta):
[1000, 1, 1, 1, 1, 1]
Step 2: 计算二阶差值(Delta-of-Delta):
[1000, 1, 0, 0, 0, 0]
存储:只需存第一个值 + 第一个 Delta + 后续的 0
为什么有效:
- 时序数据采集间隔通常固定(1s / 5s / 1min)
- 二阶差值大多为 0 或非常小的数
- 0 和小值可以用极少位数表示(Simple8B 打包)
非等间隔情况:
时间戳: [1000, 1001, 1003, 1004, 1006]
Delta: [1000, 1, 2, 1, 2]
D-of-D: [1000, 1, 1, -1, 1]
→ 虽然不全是 0,但数值仍然很小
4. 整数编码:ZigZag + Simple8B
ZigZag 编码(处理有符号整数):
问题:-1 的补码表示需要 64 位全 1
ZigZag 映射:有符号 → 无符号
0 → 0
-1 → 1
1 → 2
-2 → 3
2 → 4
...
效果:小绝对值映射为小无符号数
Simple8B 编码(紧凑打包):
将多个小值打包到一个 64-bit 整数中:
┌─────────────────────────────────────────────────────────┐
│ 64-bit word │
│ │
│ [4-bit selector] [60-bit payload] │
│ │
│ selector 决定 payload 的解读方式: │
│ selector=1: 60 × 1-bit 值(60个布尔值) │
│ selector=2: 30 × 2-bit 值(30个0~3的数) │
│ selector=3: 20 × 3-bit 值 │
│ ... │
│ selector=15: 4 × 15-bit 值 │
└─────────────────────────────────────────────────────────┘
效果:如果 Delta-of-Delta 都是 0,60 个值只占 8 字节
5. 浮点数编码:XOR 差分
XOR 差分编码(Gorilla 算法变体):
原始浮点数:[25.3, 25.4, 25.3, 25.5, 25.4]
IEEE 754 二进制表示(示意):
25.3 → 0 10000011 10010100110...
25.4 → 0 10000011 10010110011...
XOR 编码:
第一个值: 完整存储
后续值: current XOR previous
25.3 XOR 25.4 = 0000...00010001...(大部分位为 0)
存储 XOR 结果:
- 如果 XOR = 0(值未变)→ 存 1 bit: '0'
- 如果 XOR ≠ 0 → 存前导零数 + 有效位数 + 有效位
为什么有效:
- 传感器数据通常缓慢变化(温度 25.3 → 25.4)
- 相邻值的 IEEE 754 表示大部分位相同
- XOR 后只有少量位不同 → 极少字节即可表示
6. 布尔值编码:Bit Packing
BOOL 列编码:
原始:[true, false, true, true, false, true, true, true]
Bit Packing:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 0 │ 1 │ 1 │ 0 │ 1 │ 1 │ 1 │ = 1 字节
└───┴───┴───┴───┴───┴───┴───┴───┘
8 个 BOOL 值 → 1 字节(压缩率 8:1)
7. 字符串编码
BINARY/NCHAR 列编码策略:
策略一:字典编码(低基数)
原始值:["OK", "ERROR", "OK", "OK", "ERROR", "OK"]
字典:{0: "OK", 1: "ERROR"}
编码后:[0, 0, 0, 0, 1, 0] → 每个值仅 1~2 字节
适用:状态码、枚举值、设备型号等重复度高的列
策略二:前缀压缩 + LZ4(高基数)
原始值:["sensor_001_temp", "sensor_001_humi", ...]
→ 直接使用 LZ4/ZSTD 压缩(利用公共前缀)
适用:设备 ID、日志文本等
8. NULL 值处理
NULL Bitmap 编码:
每列有一个位图标记 NULL 位置:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ = NULL 位图
└───┴───┴───┴───┴───┴───┴───┴───┘
V V N V V N V V
V=有效值,N=NULL
NULL 值的列中:
- Values 数组只存非 NULL 值? 不是------
- TDengine 为 NULL 位置仍保留占位空间(简化随机访问)
- 但连续 NULL 区域在二阶压缩时会被高效压缩
全 NULL 优化:
- 如果整列全是 NULL → flag 标记 HAS_NONE
- 不存储 Values 数组(0 字节)
9. 压缩率参考
| 数据类型 | 场景 | 典型压缩率(COMP=2) |
|---|---|---|
| TIMESTAMP | 等间隔采集 | 20:1 ~ 50:1 |
| INT | 缓慢变化(如转速) | 5:1 ~ 15:1 |
| FLOAT | 传感器数据 | 3:1 ~ 8:1 |
| BOOL | 开关状态 | 8:1 ~ 16:1 |
| BINARY | 低基数枚举 | 10:1 ~ 30:1 |
| BINARY | 高基数文本 | 2:1 ~ 4:1 |
综合压缩率 :典型 IoT 场景整体压缩率在 6:1 ~ 20:1 之间。
代码示例
观察压缩效果
sql
-- 查看超级表磁盘占用及压缩率
show table distributed power.meters\G;
配置压缩参数
sql
-- 高压缩率配置(存储优先)
CREATE DATABASE archive COMP 2;
-- 低延迟配置(性能优先)
CREATE DATABASE realtime COMP 1;
-- 不压缩(调试/对比用)
CREATE DATABASE debug_db COMP 0;
性能考量
压缩与查询延迟的权衡
| COMP | 写入速度 | 磁盘占用 | 查询解码开销 |
|---|---|---|---|
| 0 | 最快(无压缩) | 最大 | 无 |
| 1 | 快(轻量编码) | 中等 | 低 |
| 2 | 中(双层压缩) | 最小 | 中 |
为什么 COMP=2 查询不一定慢?
压缩的隐藏收益:
无压缩(COMP=0)查询一个块:
磁盘读取 400KB → 耗时 4ms(假设 100MB/s)
解压:0ms
总计:4ms
COMP=2 查询一个块:
磁盘读取 50KB → 耗时 0.5ms
LZ4 解压 50KB → 耗时 0.1ms
一阶解码 → 耗时 0.1ms
总计:0.7ms ← 反而更快!
原因:磁盘 I/O 通常是瓶颈
压缩减少 I/O 量 > 增加的 CPU 解码时间
→ 对 I/O 受限的查询,压缩反而加速
编码选择对 CPU 的影响
| 编码算法 | 编码速度 | 解码速度 | 说明 |
|---|---|---|---|
| Delta-of-Delta | ~2 GB/s | ~3 GB/s | 极轻量 |
| Simple8B | ~1.5 GB/s | ~2.5 GB/s | 位操作为主 |
| XOR 差分 | ~1 GB/s | ~2 GB/s | 位操作+分支 |
| LZ4 | ~400 MB/s | ~2 GB/s | 解码远快于编码 |
| ZSTD | ~100 MB/s | ~800 MB/s | 高压缩但编解码慢 |
FAQ
Q1: 能否为不同列指定不同的压缩算法?
TDengine v3.x 中压缩算法由系统根据列类型自动选择,用户不能逐列指定编码方式。COMP 参数是数据库级别的开关。
Q2: COMP=2 写入是否明显变慢?
通常不明显。一阶编码开销极低(位操作),二阶压缩(LZ4)也设计为高速。在 CPU 充足的环境下,压缩减少的磁盘写入量可能反而提升写入吞吐。
Q3: 修改 COMP 后旧数据会重新压缩吗?
不会。COMP 参数只影响后续写入的新数据块。已存储的数据块保持原压缩格式。如需统一压缩格式,可以执行 COMPACT DATABASE 触发重写。
Q4: TSZ 有损压缩是什么?
TSZ 是针对浮点数的有损压缩算法(企业版可选),允许在指定精度范围内丢失最低有效位,换取更高的压缩率。适用于对精度要求不高的监控数据(如温度精确到 0.1°C 即可)。
参考
系统构架篇
- 01-《TDengine 整体架构全景》
- 02-《集群拓扑深度解析》
- 03-《MNode 内部机制深度解析》
- 04-《RPC 通信层深度解析》
- 05-《VNode 生命周期》
- 06-《RAFT 共识协议》
- 07-《端到端的消息流》
数据模型
- 01-《数据库创建与参数详解》
- 02-《超级表/子表/普通表》
- 03-《支持数据类型深度解析》
- 04-《TDengine Tag 设计哲学与 Schema 变更机制》
- 05-《TDengine 虚拟表实现原理》
存储引擎
- 01-《TDengine 存储引擎概览》
- 02-《TDengine MemTable 深度解析》
- 03-《TDengine WAL 预写日志机制》
- 04-《TDengine 数据文件格式》
- 05-《TDengine Commit 与 Flush 机制 》
- 06-《TDengine Compaction 合并策略 》
- 07-《TDengine 数据保留与 TTL》
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。
