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