时序数据压缩:增量编码与列存的协同原理
时序数据的可压缩本质
时序数据之所以能实现远超通用数据库的压缩比,根源在于它有三个极其显著的统计特征:时间戳单调递增、相邻数值高度相关、重复值密集。
数据类型 | 时间戳单调性 | 数值相关性 | 重复率 | 典型原始大小(64点) | 典型压缩后大小 |
温度传感器 | 严格单调 | 相邻点相关系数 > 0.95 | 低 | 1280B | 150~250B |
压力传感器 | 严格单调 | 相关系数 > 0.90 | 中 | 1280B | 200~350B |
开关状态 | 严格单调 | 低(跳变时) | 极高(99.9%是0) | 1280B | 20~50B |
振动波形 | 严格单调 | 中等(周期性) | 低 | 1280B | 400~600B |
GPS坐标 | 严格单调 | 中 | 低 | 1920B | 300~500B |
同样是64个原始数据点,温度数据的压缩比可以到8:1甚至15:1,而通用数据库对随机数据的压缩率通常只有2:1~3:1。差异就在于时序数据库识别并利用了数据的时序特征。
增量编码系列算法
针对时间戳的单调性,最直接的方案是增量编码(Delta Encoding)——不存储每个点的绝对时间戳,只存它与前一个点的差值。对于固定采样率的传感器(1秒/次),这个差值几乎恒为1,000,000微秒(毫秒精度下),只需存储一次基准值+N-1个小整数。
进一步的优化是DoubleDelta编码:不仅对时间戳做一次差分,还对差值序列再做一次差分。对于时间戳存在微小抖动的场景(网络传输延迟导致±5ms波动),差值序列仍然有强相关性。
时间戳序列(ms) | Delta | DoubleDelta | 存储位数(假设10位/值) |
1000, 2000, 3001, 4000, 5002, 6000 | 1000, 1001, 999, 1002, 998 | 1, -2, 3, -4 | 6个差值×可变长度(实际 < 平均10bit) |
对于数值列,类似的思想发展出了For(Frame of Reference)编码:存储该列的最小值作为基准值,每个点只存储它与基准值的差值。如果一个温度序列在25.3-25.8之间波动,基准值取25.3后,差值全部在0-0.5之间,可以用更小的整数表示。
DFCM(Differential Frame of Reference with Compression Mask)是For的进阶方案,额外引入一个Compression Mask位平面标记哪些点可以被高压缩。
浮点数的特殊挑战
时序数据中大量是浮点数(IEEE 754格式),传统增量编码直接应用在浮点表示上效果很差,因为浮点的内部表示不是连续的——25.0和25.1在浮点表示上的位差异可能比25.0和26.0还大。
XOR Encoding巧妙地解决了这个问题。核心观察是:两个数值如果很接近,它们的IEEE 754表示做XOR操作后,前导零(Leading Zeros)和尾随零(Trailing Zeros)都很多,有效位(Significant Bits)长度很短。编码时存储前导零长度+有效位长度+有效位内容,相邻值的前导零长度相同还可以进一步压缩。
25.0 的 IEEE 754 表示:
010000011 10010000000000000000000 (32位,单精度)
25.1 的 IEEE 754 表示:
010000011 10010001100110011001100
XOR 结果:
000000000 00000001100110011001100
前导零 = 9位,有效位长度 = 23位 → 存储只需要记录这两个数字和有效位内容
XOR编码与DoubleDelta结合(先对数值做一次Delta,再对Delta序列做XOR),就是业界著名的Gorilla Compression算法——Facebook在其Gorilla时序数据库中首创,TDengine和Prometheus TSDB都采用了类似思路。
熵编码:最后一公里
增量编码、XOR编码等步骤完成后,输出的是一串变长整数。这些整数的分布并不均匀——小数值出现概率远高于大数值,因为相邻时间戳的抖动通常在几个毫秒以内。熵编码(Entropy Coding)就是利用这种概率分布的不均匀性做最后压缩。
熵编码算法 | 特点 | 适用场景 |
Huffman Coding | 统计每个值的出现频率,频率高的用短码 | 值的概率分布极不均匀时最优 |
Arithmetic Coding | 编码效率略高于Huffman,但实现复杂 | 需要最优压缩率时 |
LZ4 | 快速压缩/解压,无字典开销 | 热数据、需要高速解压的场景 |
ZSTD | LZ4的下一代,压缩率更高,速度相当 | 平衡压缩率和速度的通用选择 |
GZIP/DEFLATE | 经典算法,兼容性最好 | 需要跨平台兼容时 |
TDengine在增量编码和XOR编码完成后,用LZ4做熵编码层。原因很直接:时序数据的查询端需要频繁解压,LZ4的解压速度(单核>500MB/s)远优于ZSTD/GZIP,实际工程中压缩率差异(约10%)远不如解压速度重要。
列式存储的协同效应
如果把压缩算法比作”手术刀”,列式存储就是”手术台”——它让手术刀能精准命中病灶。时序数据按时间窗切块存储,每个块内同一列的数值在时间上是连续的,这种物理存储布局让增量编码和XOR编码的效果最大化。
行式存储的问题在于,一条记录包含多个列的数值,每个数值的分布特征混合在一起,无法针对性优化。列式存储把温度列、压力列、时间戳列分开存,每一列内部的数值特征极其单一:温度列都是单调或缓变的浮点数,时间戳列都是严格单调的整数序列。
存储方式 | 同一列数据的聚集度 | 压缩算法适配度 | 典型压缩率 |
行式存储 | 低(多列混合) | 通用压缩器 | 2:1 ~ 4:1 |
列式存储 | 高(单类型聚集) | 增量+XOR+熵编码 | 10:1 ~ 20:1 |
TDengine的存储引擎按超级表维度切块,每个数据块包含多个子表在同一时间窗口内的记录,但同一列的数据在物理上是连续存放的。这就是为什么TDengine在压缩率上能显著优于行式存储的数据库。
压缩率 vs CPU开销
任何压缩都是空间换时间的Trade-off。压缩率越高,编码和解码的CPU开销越大。
压缩层级 | 额外CPU开销(编码+解码) | 压缩率增益 | 推荐配置 |
仅增量编码 | < 5% | 3:1 ~ 5:1 | 低延迟场景 |
增量 + XOR | 10% ~ 20% | 8:1 ~ 12:1 | 通用场景(TDengine默认) |
增量 + XOR + ZSTD | 30% ~ 50% | 12:1 ~ 18:1 | 冷数据归档 |
增量 + XOR + GZIP | > 60% | 15:1 ~ 20:1 | 长期冷存储 |
TDengine允许按数据块配置不同的压缩策略——热数据用轻量压缩保证查询速度,冷数据用高强度压缩最大化存储效率。
结语
时序数据的高压缩率不是一个黑魔法,而是对数据本质特征逐层挖掘的结果。DoubleDelta抓住了时间戳的单调性,XOR编码抓住了浮点数的接近性,列式存储让同类型数据聚集,熵编码吃掉最后一层统计冗余。TDengine的压缩实现把这些原理组合在一起,在CPU开销和压缩率之间取了一个工程化的平衡。对于任何构建或评估时序数据库的团队,理解压缩原理不是优化手段的选择,而是从根本上判断架构设计方向是否正确的标尺。

























