TimescaleDB 压缩:Hypercore 与列式存储

TimescaleDB 通过使用 hypercore 引擎,实现了时间序列数据最高可达 98% 的压缩率。不同于通用压缩,hypercore 采用混合行‑列方式,利用针对时间序列数据数学特性(如单调性和重复性)定制的专用算法。

Hypercore 与 PostgreSQL TOAST 的对比

TimescaleDB 的压缩是对 PostgreSQL 内置 TOAST(Oversized‑Attribute Storage Technique)的一种补充,而不是替代。TOAST 处理单个超大值(例如长字符串或 JSONB),当其超过特定阈值时进行存储;而 hypercore 优化 跨行模式

功能 TOAST(原生 PostgreSQL) TimescaleDB hypercore
设计目标 单个值 > 2 KB 时间序列中的跨行模式
触发条件 行超过 TOAST_TUPLE_THRESHOLD 按块策略(例如,超过 7 天的旧块)
支持类型 仅可变长度(textjsonb 等) 所有数据类型
算法 pglzlz4 Delta、Delta‑of‑Delta、Simple‑8b、RLE、基于 XOR、字典
粒度 按值 按批次(约 1000 行)
数据结构 将值视为不透明字节 利用数值结构和重复性
传感器浮点比例 ~1.0× 10‑20×
时间戳比例 ~1.0× 50‑100×
文本比例 2‑3× 5‑10×

列式压缩的工作原理

Hypercore 将较旧的数据块从面向快速 INSERT 的行式格式转换为列式格式。在此过程中,行被分组成最多 1,000 行的批次。每个批次作为压缩表中的单行存储,列以数组形式呈现。

专用压缩算法

TimescaleDB 根据列的数据类型选择压缩算法,以实现最高效率:

  • 整数、时间戳和布尔值:使用 增量编码(存储值之间的差异)、增量的增量(存储差异的变化,对规则间隔为 0)、simple‑8b游程编码 (RLE) 的组合。
  • 浮点数(如温度/振动):采用 基于 XOR 的压缩(基于 Gorilla 算法)。通过对相邻浮点数进行 XOR,发动机只存储有效位,忽略长串零位。
  • JSONB:采用两层方式:首先使用字典压缩重复值,若不存在重复则回退到 PostgreSQL TOAST。
  • 字符串及其他类型:使用 字典压缩,字典索引本身再通过 simple‑8b 与 RLE 进一步压缩。

示例:Delta 与游程编码

对于在多行中重复的 machine_id,RLE 只存储一次该值并附带计数(例如 MACHINE_001 × 5),而不是重复五次字符串。对于规则间隔的时间戳,增量的增量编码可以将每个值的存储需求降至几乎为零字节。

使用 segmentbyorderby 优化压缩

有两个参数对行如何分批以及压缩效果至关重要:

  • segmentby:定义在一个批次内共享的列(例如 machine_id)。该列的值在每个批次只存储一次。查询规划器利用此元数据跳过不满足 WHERE 条件的整批数据。
  • orderby:定义批次内部的排序顺序(通常为 time DESC)。按时间排序可最大化 delta 与 delta‑of‑delta 编码的效果,因为相邻值更可能相似。

配置示例:

ALTER TABLE iot_sensor_data SET (
  timescaledb.orderby = 'time DESC',
  timescaledb.segmentby = 'machine_id'
);

最佳实践: 每个 segment 在每块中应至少包含 100 行,最佳范围为每块 100‑10,000 个唯一的 segmentby 值。

对查询性能的影响

对于大多数时间序列工作负载,压缩 通过将 I/O 需求降低 10‑20 倍,提升查询速度。

性能提升

  • 基于时间范围的扫描 并伴随聚合(SUMAVGMAX)。
  • segmentby 列上过滤的查询
  • 大范围的顺序扫描

性能权衡

  • 单行的 点查询 可能会变慢。
  • 对压缩块的 UPDATE/DELETE 需要先解压‑修改‑再压缩的循环。
  • 对高基数列且 没有 segmentby 过滤 的查询可能出现性能下降。

实际基准

在一次使用 MQTT 传感器数据的生产测试中,按 id 和窄时间范围的点读取实现了 28 倍的执行时间加速(10.2 ms 降至 0.36 ms)以及 42.8 倍的压缩率(308 MB 降至 7.2 MB),从行存储迁移到列存储块后得到的结果。

为什么列存储更快

  1. 稀疏 MinMax 索引:TimescaleDB 自动在 (segmentby_col, _ts_meta_min_1, _ts_meta_max_1) 上构建索引。该索引使引擎能够在不读取实际数据的情况下剔除整批。
  2. 原生过滤:由于具有相同 id 的行在物理上已分组,引擎能够立即命中正确的 segment,无需在 (id, time) 上建立大型 B‑tree 索引。
  3. 向量化执行:对时间范围的操作以 1,000 行为单位批处理,而非逐行处理,降低 CPU 开销。

Sources