6月15日、Roszi Levente氏が「TimescaleDB Compression: Hypercore and Columnar Storage with up to 98% Ratio in PostgreSQL」と題した記事を公開した。この記事では、時系列データベース拡張TimescaleDBが時系列データに対して最大98%の圧縮を実現する技術的な仕組みについて詳しく紹介されている。
TimescaleDBはPostgreSQLの拡張機能で、IoTセンサーデータ、金融市場データ、アプリケーションメトリクスなど、時系列データの管理に特化している。時系列データは急速に増大するため、ストレージコストとクエリパフォーマンスの両立が課題となる。TimescaleDBの圧縮機能は、標準PostgreSQLのTOASTでは圧縮できない浮動小数点やタイムスタンプを10-100倍に圧縮し、同時にクエリも高速化する点が特徴だ。
PostgreSQLのTOASTとは根本的に異なる圧縮
TimescaleDBは時系列データに対して最大98%の圧縮率を達成できる。この圧縮を実現しているのがhypercoreと呼ばれるハイブリッド行列エンジンだ。delta encoding、delta-of-delta、Gorilla XOR、run-length encoding(RLE)といった専用アルゴリズムを組み合わせて使用する。
PostgreSQLにはTOAST(The Oversized-Attribute Storage Technique)という組み込みの圧縮機構がある。しかしTOASTとTimescaleDBの圧縮は、解決する問題が根本的に異なる。TOASTは個別の大きな値(長い文字列、jsonb、bytea)を扱うのに対し、TimescaleDBは時系列データの行をまたいだパターンを最適化する。両者は競合ではなく補完関係にあり、TimescaleDBも内部的にはTOASTを一部のデータ型のフォールバックとして使用している。
以下の比較表が、両者の違いを明確に示している:
| 特性 | TOAST(標準PostgreSQL) | TimescaleDB hypercore |
|---|---|---|
| 設計目標 | 2KB超の個別値 | 時系列データの行間パターン |
| トリガー | 行が約2KBを超える | チャンク単位のポリシー(例:7日より古い) |
| 対応型 | 可変長のみ(text、jsonb、bytea、numeric) | すべてのデータ型 |
| アルゴリズム | pglz(デフォルト)、lz4(PG14以降) | delta encoding、delta-of-delta、simple-8b、RLE、XOR、辞書圧縮の組み合わせ |
| 圧縮粒度 | 値単位(1値=1バイトストリーム) | バッチ単位(約1000行を一緒に) |
| センサー浮動小数点の圧縮率 | 約1.0×(圧縮なし) | 10-20× |
| タイムスタンプの圧縮率 | 約1.0×(固定長型のため圧縮なし) | 50-100×(定期間隔のdelta-of-delta) |
典型的なIoTワークロード(浮動小数点とタイムスタンプ)において、TOASTが全く圧縮しない列でTimescaleDBは10-100倍の圧縮率を達成する。
Hypercoreエンジンとカラムナー圧縮の仕組み
Hypercoreは、新しいデータをPostgreSQLの行ベースチャンクに格納し(高速なINSERTとUPDATE)、古いチャンクを自動的にカラムナー圧縮フォーマットに変換するハイブリッドエンジンだ。
圧縮時、チャンクの行は最大1000行のバッチにグループ化される。各バッチは圧縮テーブル内で1行となり、各列は配列として格納される。各圧縮バッチは:
- 列ごとに最大1000値の圧縮配列をカプセル化
- バッチ内部では列優先フォーマットを使用し、行全体を読まずに個別の列を選択可能
- 列レベルの高度な圧縮技術(RLE、delta encoding、Gorilla圧縮)を適用
具体例:Delta Encodingの威力
以下の生データを考える:
| time | machine_id | sensor_type | value |
|---|---|---|---|
| 12:00:00 | MACHINE_001 | temp | 72.5 |
| 12:00:05 | MACHINE_001 | temp | 72.7 |
| 12:00:10 | MACHINE_001 | temp | 72.4 |
| 12:00:15 | MACHINE_001 | temp | 72.6 |
| 12:00:20 | MACHINE_001 | temp | 72.5 |
Delta encodingでは、前の値からの変化量だけを保存する。時間が一定間隔(常に5秒)の場合、delta-of-deltaは0になり、数ビットで表現できる。
Run-length encoding(RLE)は、同じ値が連続する場合に効果を発揮する。上記の例でmachine_idとsensor_typeにRLEを適用すると:
| machine_id | sensor_type |
|---|---|
| MACHINE_001 × 5 | temp × 5 |
MACHINE_001の5コピー(約55バイト)が、1つの値とカウンター(約15バイト)に圧縮される。数百万行で同じ値が共有される場合、節約効果は莫大だ。
最終的な圧縮表現:
| column | technique | representation |
|---|---|---|
| time | delta-of-delta | 12:00:00, +5s, 0, 0, 0 |
| machine_id | RLE | MACHINE_001 × 5 |
| sensor_type | RLE | temp × 5 |
| value | delta encoding | 72.5, +0.2, -0.3, +0.2, -0.1 |
TimescaleDBは列の型に応じてアルゴリズムを選択する:
- 整数、タイムスタンプ、ブール値:delta encoding、delta-of-delta、simple-8b、RLEの組み合わせ
- 繰り返しが少ない列(温度や振動の浮動小数点):XORベース圧縮(Gorillaアルゴリズムベース)+辞書圧縮
- JSONB:辞書圧縮+TOAST(pglzまたはlz4)のフォールバック
- その他(文字列など):辞書圧縮
sensor_typeのような値('TEMPERATURE'/'SPEED'/'PRESSURE')は3要素の辞書+RLEで優れた圧縮率を達成する。一方、UUID等の高エントロピー列は圧縮が困難だ。
segmentbyとorderby:最重要パラメータ
この2つのパラメータは、行をバッチにグループ化する方法を決定するため、慎重に選ぶ必要がある。
- **
segmentby**:バッチ全体で共有される列(例:machine_idやsensor_id)。値はバッチごとに1回だけ保存され、配列にはならない。プランナーはWHERE句に一致しないバッチ全体をスキップできる - **
orderby**:バッチ内のソート順(通常はtime DESC)。時間でソートすると、delta encodingとdelta-of-deltaが最大の効果を発揮する
ALTER TABLE iot_sensor_data SET (
timescaledb.orderby = 'time DESC',
timescaledb.segmentby = 'machine_id'
);
WHERE machine_id = '...' AND time BETWEEN ...というフィルタを持つクエリは、segmentbyを適切に設定すると桁違いに高速化する。プランナーがメタデータに基づいて他のマシンのバッチをスキップするためだ。
公式ドキュメントの推奨ルール:各セグメントはチャンク内に最低100行含むべきで、理想的にはチャンクあたり100~10,000のユニークなsegmentby値。
圧縮はクエリパフォーマンスに何をもたらすか
よくある質問:圧縮はクエリを遅くするか?
短い答え:典型的な時系列クエリでは高速化する。
高速化するクエリ(ワークロードの大半):
- 時間範囲スキャンと集約(時間バケットごとの
SUM、AVG、MAX) segmentby列のフィルタを持つクエリ- 大範囲のシーケンシャルスキャン
カラムナー圧縮はI/Oを10-20倍削減する。非圧縮で1GB読む場合と圧縮で100MB読む場合では、ディスク読み取り、メモリ、デシリアライゼーションのCPU使用が異なる。
遅くなるクエリ(時系列では稀):
- 単一行のポイントルックアップ(
WHERE time = '...' AND id = X) - 圧縮チャンクへのUPDATE/DELETE(解凍→変更→再圧縮サイクル)
- カーディナリティが高い
segmentby列でフィルタなしのクエリ
実装方法
-- IoTセンサー監視用のカラムストア設定
ALTER TABLE iot_sensor_data SET (
timescaledb.compress,
timescaledb.segmentby = 'machine_id',
timescaledb.orderby = 'time DESC'
);
-- 7日より古いチャンクを自動変換するポリシー
SELECT add_columnstore_policy('iot_sensor_data', after => INTERVAL '7 days');
実環境での例
著者のmqtt_dataテーブルには約180のユニークなid値があり、チャンクごとに4,000~113,000行が含まれる。設定:
ALTER TABLE mqtt_data SET (
timescaledb.enable_columnstore = true,
timescaledb.segmentby = 'id',
timescaledb.orderby = 'time DESC'
);
5分間の時間範囲で特定のidを検索する本番タイプのクエリで、rowstoreチャンク(2.3GB)とcolumnstoreチャンク(7.2MB)を比較した結果、実行時間は10.2msから0.8msに短縮され、スキャンするデータ量も大幅に減少した。
圧縮により、ストレージコストが削減されるだけでなく、適切に設定すればクエリパフォーマンスも向上する。segmentbyとorderbyの選択が鍵となる。
詳細はTimescaleDB Compression: Hypercore and Columnar Storage with up to 98% Ratio in PostgreSQLを参照していただきたい。