分类: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(默认) | 双层压缩 | 一阶编码 + 二阶通用压缩 |
-- 创建数据库时指定压缩级别CREATEDATABASEpower COMP2;-- 修改压缩级别(影响后续写入,不影响已有数据)ALTERDATABASEpower COMP1;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之间。
代码示例
观察压缩效果
-- 查看超级表磁盘占用及压缩率showtabledistributedpower.meters\G;配置压缩参数
-- 高压缩率配置(存储优先)CREATEDATABASEarchive COMP2;-- 低延迟配置(性能优先)CREATEDATABASErealtime COMP1;-- 不压缩(调试/对比用)CREATEDATABASEdebug_db COMP0;性能考量
压缩与查询延迟的权衡
| 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 提供实时分析、可视化、事件管理与报警等功能。