タイムシリーズ・データのPostgreSQLスケーリング:TimescaleDB実践ガイド

Database tutorial - IT technology blog
Database tutorial - IT technology blog

標準的なPostgreSQLの限界

以前、約46,000平方メートル(50万平方フィート)の倉庫にある温度センサーを追跡するプロジェクトを管理していました。最初は標準的なPostgreSQLテーブルで問題なく動作していましたが、行数が5,000万件を超えたあたりから、かつて20ミリ秒で終わっていたクエリが15秒以上かかるようになりました。積極的なインデックス作成を行っても、データベースの動作は重いままでした。

このようなパフォーマンスの低下は、タイムシリーズ(時系列)データの標準的なライフサイクルと言えます。株価、システムログ、IoTメトリクスなど、扱うデータ量が膨大になると、従来の型のリレーショナル構造では対応できなくなります。Bツリーインデックスが肥大化してRAMに収まらなくなると、ディスクI/Oが急増し、パフォーマンスは急落します。高速な読み取り速度を維持しながら、毎秒1万件の書き込みを処理できるように標準的なテーブルをスケールさせるのは、負け戦に近いものです。

TimescaleDBは、PostgreSQLを拡張することでこの問題を解決します。既存のSQLクエリ、ツール、ライブラリをそのまま使いながら、Postgresを高性能なタイムシリーズ・エンジンへと変貌させます。

ハイパーテーブルがスケーリング問題を解決する仕組み

TimescaleDBはPostgreSQLを置き換えるものではなく、その内部で動作します。その核となる革新的な構造がハイパーテーブル(Hypertables)です。

「チャンク」による自動パーティショニング

ハイパーテーブルは単一の仮想テーブルとして機能しますが、内部的にはデータをチャンク(chunks)と呼ばれる多くの小さな物理テーブルに分割します。例えば、1日単位でパーティショニングするようにハイパーテーブルを設定すると、TimescaleDBは24時間ごとに新しいチャンクを自動的に作成します。

このアーキテクチャが高速化の鍵です。チャンクが小さいため、そのインデックスもサーバーのRAMに収まるサイズに保たれます。直近1時間のデータをクエリする場合、エンジンは今日の特定のチャンクだけにアクセスします。データの99%は無視されるため、1億行目の挿入も1行目と同じくらい高速に行えます。

継続的集計(Continuous Aggregates)の威力

1ヶ月にわたる500台のセンサーの1時間ごとの平均温度を計算するのは、非常に重い処理です。ダッシュボードが更新されるたびにこの計算を実行すると、CPUリソースを大幅に浪費します。継続的集計(Continuous Aggregates)はマテリアライズド・ビューのように機能しますが、新しいデータが届くと自動的に更新されます。バックグラウンドで重い処理を済ませておくことで、10秒かかる計算を5ミリ秒のルックアップに短縮できます。

インストールと初期設定

ローカル開発においては、Dockerを利用するのが最も簡単です。システムの依存関係やライブラリパスの管理に悩まされることもありません。

1. DockerでTimescaleDBを起動する

以下のコマンドを実行して、TimescaleDBエクステンションが事前設定されたコンテナを起動します。

docker run -d --name timescaledb -p 5432:5432 \
  -e POSTGRES_PASSWORD=mysecretpassword \
  timescale/timescaledb-ha:pg16-latest

もしベアメタルのUbuntuにインストールする場合は、必ずtimescaledb-tuneを実行してください。このユーティリティはシステムのRAMとCPUを調査し、postgresql.confファイルに最適な設定を推奨してくれます。

2. エクステンションの有効化

psqlやDBeaver経由で接続し、以下のコマンドを実行してタイムシリーズ機能を有効にします。

CREATE EXTENSION IF NOT EXISTS timescaledb;

3. 初めてのハイパーテーブルの作成

気象メトリクス用のテーブルを作成してみましょう。まずは標準的なSQLテーブルの定義から始めます。

CREATE TABLE weather_metrics (
  time        TIMESTAMPTZ       NOT NULL,
  city_id     INT               NOT NULL,
  temperature DOUBLE PRECISION  NULL,
  humidity    DOUBLE PRECISION  NULL
);

パフォーマンスのメリットを引き出すために、時間カラムを指定してハイパーテーブルに変換します。

SELECT create_hypertable('weather_metrics', 'time');

4. 効率的なデータのインポート

少量のバッチであれば、標準的なSQLのINSERTステートメントで十分です。しかし、レガシーシステムからデータを移行する場合は、巨大なCSVファイルを扱うことになるでしょう。

テスト用にデータ形式を素早く変換する必要があるとき、私は toolcraft.app/ja/tools/data/csv-to-json を使っています。ブラウザ内で完結して処理されるため、データが外部に送信されることがなく、プライバシー面でも安心です。PostgresでJSONBストレージ戦略を試行錯誤する際にも便利なショートカットになります。

5. タイムバケット(Time Bucket)によるクエリ

time_bucket関数は、TimescaleDBの強力な武器です。15分や6時間といった任意のインターバルでデータをグループ化する処理を、SQL1行で記述できます。

SELECT 
  time_bucket('15 minutes', time) AS bucket,
  avg(temperature) AS avg_temp
FROM weather_metrics
WHERE time > NOW() - INTERVAL '24 hours'
GROUP BY bucket
ORDER BY bucket DESC;

高度なパフォーマンス・チューニング

データベースが1日あたり数GB単位で肥大化し始めたら、「圧縮」と「保持(リテンション)」の2つの機能を有効にすることをお勧めします。

ネイティブ列指向圧縮

タイムシリーズ・データは繰り返しが多いため、圧縮に最適です。TimescaleDBは、行ベースのデータを非常に効率的な列指向フォーマットに変換することで、ストレージ容量を90%以上削減できることがよくあります。以下のポリシーを使用して、1週間より古いデータを圧縮します。

ALTER TABLE weather_metrics SET (
  timescaledb.compress,
  timescaledb.compress_segmentby = 'city_id'
);

SELECT add_compression_policy('weather_metrics', INTERVAL '7 days');

データ保持(リテンション)の自動化

3年前の1秒ごとのセンサーデータが必要になるケースは稀です。テーブルをロックしてしまう複雑なDELETEスクリプトを書く代わりに、保持ポリシーを設定しましょう。以下のコマンドは、30日より古いデータチャンクを自動的に削除し、データベースを軽量かつ高速に保ちます。

SELECT add_retention_policy('weather_metrics', INTERVAL '30 days');

TimescaleDBはあなたに適しているか?

アプリケーションが時間の経過に伴うイベントを記録するものであれば、TimescaleDBは通常のPostgreSQLからの大幅なアップグレードになります。Postgresエコシステムの信頼性を維持しながら、数十億行のデータをミリ秒単位でクエリできるようになります。まずは、最も肥大化したテーブルをハイパーテーブルに変換することから始めてみてください。クエリのレイテンシとディスク使用量が即座に低下するのがわかるはずです。SQLに慣れている開発者にとって、本番環境レベルのタイムシリーズ・バックエンドを構築するのに、これ以上に速い方法はありません。

Share: