TimescaleDB Compression: Hypercore and Columnar Storage

TimescaleDBは、hypercoreエンジンを利用することで、時系列データに対して最大98%の圧縮率を実現します。汎用的な圧縮とは異なり、hypercoreは、単調増加性や繰り返しといった時系列データの数学的特性に合わせた専用のアルゴリズムを活用する、ハイブリッドな行・列(row-columnar)アプローチを採用しています。

Hypercore vs. PostgreSQL TOAST

TimescaleDBの圧縮は、PostgreSQLの組み込み機能であるTOAST (The Oversized-Attribute Storage Technique) の置き換えではなく、それを補完するものです。TOASTは特定の閾値を超える個々の大きな値(例:長い文字列やJSONB)を管理しますが、hypercoreは行をまたいだパターンを最適化します。

Feature TOAST (vanilla PostgreSQL) TimescaleDB hypercore
Design Goal 個々の値 > 2 KB 時系列における行をまたいだパターン
Trigger 行が TOAST_TUPLE_THRESHOLD を超える チャンクごとのポリシー (例: 7日以上経過)
Supported Types 可変長データのみ (text, jsonb, etc.) すべてのデータ型
Algorithms pglz, lz4 Delta, Delta-of-Delta, Simple-8b, RLE, XOR-based, Dictionary
Granularity 値ごと バッチごと (~1000 rows)
Data Structure 値を不透明なバイト列として扱う 数値構造と繰り返しを最大限に活用
Sensor Floats Ratio ~1.0× 10-20×
Timestamp Ratio ~1.0× 50-100×
Text Ratio 2-3× 5-10×

How Columnar Compression Works

Hypercoreは、古いデータチャンクを(高速なINSERTに最適化された)行ベースの形式から、列ベースの形式に変換します。このプロセスにおいて、行は最大1,000行のバッチにグループ化されます。各バッチは圧縮されたテーブル内の単一の行として保存され、列は配列として表現されます。

Specialized Compression Algorithms

TimescaleDBは、効率を最大化するために列のデータ型に基づいて圧縮アルゴリズムを選択します:

  • Integers, Timestamps, and Booleans: delta encoding (値の差分を保存)、delta-of-delta (差分の変化、つまり規則的な間隔の場合は0を保存)、simple-8b、および run-length encoding (RLE) を組み合わせて使用します。
  • Floats (e.g., temperature/vibration): XOR-based compression (Gorillaアルゴリズムに基づく) を採用しています。隣接する浮動小数点数をXORすることで、エンジンはゼロが続く部分を無視し、重要なビットのみを保存します。
  • JSONB: 2層のアプローチを採用しています。まず繰り返しの値に対して辞書(dictionary)を使用し、繰り返しがない場合はPostgreSQL TOASTにフォールバックします。
  • Strings and Other Types: dictionary compression を使用します。ここでは、辞書インデックス自体がsimple-8bやRLEを用いてさらに圧縮されます。

Example: Delta and Run-Length Encoding

多くの行で繰り返される machine_id の場合、RLEは文字列を5回繰り返す代わりに、値とカウンターを一緒に保存します (例: MACHINE_001 × 5)。規則的な間隔のタイムスタンプの場合、delta-of-deltaエンコーディングにより、値あたりのストレージ要件をほぼ0バイトに削減できます。

Optimizing Compression with segmentby and orderby

行をどのようにバッチにグループ化し、いかに効果的に圧縮するかを決定する上で、2つのパラメータが極めて重要です:

  • segmentby: バッチ全体で共有される列 (例: machine_id) を定義します。この値はバッチごとに1回だけ保存されます。クエリプランナーは、このメタデータを使用して WHERE 句に一致しないバッチ全体をスキップできます。
  • orderby: バッチ内のソート順 (通常は time DESC) を定義します。時間でソートすることで、隣接する値が似通う可能性が高くなるため、deltaおよびdelta-of-deltaエンコーディングの効率が最大化されます。

Configuration Example:

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

Best Practice: 各セグメントには、チャンクあたり少なくとも100行が含まれているべきであり、チャンクあたりの一意的な segmentby 値の最適な範囲は100–10,000です。

Impact on Query Performance

ほとんどの時系列ワークロードにおいて、圧縮はI/O要件を10–20倍削減することで、クエリ速度を向上させます。

Performance Gains

  • Range scans を時間軸に沿って集計関数 (SUM, AVG, MAX) と共に行う場合。
  • segmentby 列によるフィルタリングを行うクエリ。
  • Sequential scans を広範囲に行う場合。

Performance Trade-offs

  • 単一の行の Point lookups は、遅くなる可能性があります。
  • 圧縮されたチャンクに対する UPDATE/DELETE 操作は、解凍・修正・再圧縮のサイクルを必要とします。
  • 高カーディナリティな列に対する segmentby フィルタなし のクエリは、パフォーマンスの低下を招く可能性があります。

Real-World Benchmark

MQTTセンサーデータを使用した本番環境テストにおいて、id によるポイントリードと狭い時間範囲の指定を行った場合、行ベースのストレージから列ベースのストレージのチャンクへ移行することで、実行時間が28倍高速化 (10.2 ms から 0.36 ms へ) し、圧縮率が42.8倍 (308 MB から 7.2 MB へ) となりました。

Why Columnstore is Faster

  1. Sparse MinMax Index: TimescaleDBは、(segmentby_col, _ts_meta_min_1, _ts_meta_max_1) に対して自動的にインデックスを構築します。これにより、エンジンは実際のデータにアクセスすることなく、バッチ全体をスキップできます。
  2. Native Filtering: 同じ id を持つ行が物理的にグループ化されているため、エンジンは (id, time) に対する大きなB-treeインデックスを必要とせず、即座に正しいセグメントに到達できます。
  3. Vectorized Execution: 時間範囲に関する操作は、行ごとではなく1,000行のバッチ単位で処理されるため、CPUオーバーヘッドが削減されます。

Sources