LabHub

ブログ

TimescaleDB時系列データベース運用ガイド

한국어English日本語

1. なぜ TimescaleDB なのか

IoT センサー、インフラのメトリクス、金融の相場、アプリケーションログなど、時系列 (Time-Series) データは現代のシステムで最も速く増えるデータ型だ。このデータに共通するのは、時間軸に沿った連続生成大量の INSERT とほとんど発生しない UPDATE直近データ中心の照会という特性である。

PostgreSQL は汎用のリレーショナルデータベースとして時系列データを保存できるが、数十億行の規模になると時間範囲クエリの性能が急激に低下する。ネイティブパーティショニングで回避するとしても、パーティション管理、圧縮、集約の最適化をすべて自前で実装しなければならない運用負担が残る。

TimescaleDB は PostgreSQL の拡張 (extension) として動作しながら、この問題を正面から解決する。ハイパーテーブル (Hypertable) による自動パーティショニング、連続集約 (Continuous Aggregates)、ネイティブ圧縮、自動データ保持ポリシーなどを提供し、既存の PostgreSQL の SQL 構文、インデックス、JOIN、トランザクションをそのまま使える。

2026 年 1 月にリリースされた TimescaleDB 2.25 では、圧縮データに対する ColumnarIndexScan 実行パスが導入され、MIN/MAX/FIRST/LAST クエリが最大 289 倍速くなり、時間フィルタを含む COUNT クエリは最大 50 倍の性能向上が得られた。


2. TimescaleDB のアーキテクチャと PostgreSQL 拡張の構造

インストールと初期設定

TimescaleDB は PostgreSQL の共有ライブラリとしてロードされ、CREATE EXTENSION の 1 行で有効化される。

# Ubuntu/Debian でのインストール
sudo apt install timescaledb-2-postgresql-16

# PostgreSQL 設定ファイルに shared_preload_libraries を追加
sudo timescaledb-tune --yes

# PostgreSQL の再起動
sudo systemctl restart postgresql

# データベースで拡張を有効化
psql -d mydb -c "CREATE EXTENSION IF NOT EXISTS timescaledb;"

内部アーキテクチャ

TimescaleDB の要点は ハイパーテーブル (Hypertable) だ。ユーザーには単一のテーブルに見えるが、内部では時間軸を基準に自動分割された複数の チャンク (Chunk) で構成される。各チャンクは PostgreSQL の通常のテーブルであり、指定された時間間隔 (既定は 7 日) に該当するデータを格納する。

ユーザーから見た姿:
+-------------------------------------------+
|        metrics (Hypertable)                |
|  SELECT * FROM metrics                     |
|  WHERE time > now() - interval '1 hour'    |
+-------------------------------------------+

内部構造:
+-----------+-----------+-----------+-----------+
| chunk_1   | chunk_2   | chunk_3   | chunk_4   |
| 02-15~21  | 02-22~28  | 03-01~06  | 03-07~now |
| [圧縮済み] | [圧縮済み] | [圧縮予定] | [アクティブ] |
+-----------+-----------+-----------+-----------+

このアーキテクチャがもたらす主な利点は次のとおり。


3. ハイパーテーブルの設計とチャンク管理

ハイパーテーブルの作成

-- 通常のテーブルを作成
CREATE TABLE sensor_data (
    time        TIMESTAMPTZ NOT NULL,
    device_id   TEXT        NOT NULL,
    temperature DOUBLE PRECISION,
    humidity    DOUBLE PRECISION,
    battery     DOUBLE PRECISION
);

-- ハイパーテーブルへ変換 (チャンク間隔 1 日)
SELECT create_hypertable(
    'sensor_data',
    by_range('time', INTERVAL '1 day')
);

-- 空間パーティショニングの追加 (任意、大規模環境向け)
SELECT add_dimension(
    'sensor_data',
    by_hash('device_id', 4)
);

チャンク間隔 (Chunk Interval) 設定ガイド

チャンク間隔は TimescaleDB の性能を左右する中心的なチューニングポイントだ。アクティブなチャンクのインデックスがメモリに常駐できてこそ、INSERT も SELECT も速くなる。

1 日あたりの INSERT 行数推奨チャンク間隔理由
100万 未満7 日 (既定値)チャンク数を減らしメタデータのオーバーヘッドを最小化
100万 ~ 1,000万1 日インデックスサイズとチャンク管理の均衡
1,000万 以上6 時間 ~ 12 時間アクティブなインデックスがメモリに常駐するのを保証
1億 以上1 時間 ~ 3 時間チャンクごとのインデックスを RAM の 25% 以内に維持
-- チャンク間隔の変更
SELECT set_chunk_time_interval('sensor_data', INTERVAL '12 hours');

-- 現在のチャンク一覧を確認
SELECT chunk_name, range_start, range_end,
       pg_size_pretty(total_bytes) AS size,
       is_compressed
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_start DESC
LIMIT 10;

チャンク管理のモニタリング

-- ハイパーテーブル別の詳細情報
SELECT hypertable_name,
       num_chunks,
       pg_size_pretty(hypertable_size(format('%I.%I',
           hypertable_schema, hypertable_name)::regclass)) AS total_size,
       pg_size_pretty(hypertable_size(format('%I.%I',
           hypertable_schema, hypertable_name)::regclass)
           - pg_total_relation_size(format('%I.%I',
               hypertable_schema, hypertable_name)::regclass)) AS chunk_size
FROM timescaledb_information.hypertables;

-- チャンク数が過剰なハイパーテーブルを確認 (1000 個以上なら注意)
SELECT hypertable_name, num_chunks
FROM timescaledb_information.hypertables
WHERE num_chunks > 1000;

4. 連続集約 (Continuous Aggregates) の設定と活用

連続集約は、時系列データの集約結果を事前計算して増分更新することで、ダッシュボードのクエリ性能を劇的に向上させる機能だ。新しいデータが追加されたとき、全体を再計算せず、変更された部分だけを更新する。

連続集約の作成

-- 1 時間単位の集約ビューを作成
CREATE MATERIALIZED VIEW sensor_hourly
WITH (timescaledb.continuous) AS
SELECT
    device_id,
    time_bucket('1 hour', time) AS bucket,
    AVG(temperature)   AS avg_temp,
    MAX(temperature)   AS max_temp,
    MIN(temperature)   AS min_temp,
    AVG(humidity)      AS avg_humidity,
    COUNT(*)           AS sample_count
FROM sensor_data
GROUP BY device_id, bucket
WITH NO DATA;

-- 1 日単位の集約 (階層型連続集約 - 連続集約の上にさらに連続集約)
CREATE MATERIALIZED VIEW sensor_daily
WITH (timescaledb.continuous) AS
SELECT
    device_id,
    time_bucket('1 day', bucket) AS bucket,
    AVG(avg_temp)      AS avg_temp,
    MAX(max_temp)      AS max_temp,
    MIN(min_temp)      AS min_temp,
    AVG(avg_humidity)  AS avg_humidity,
    SUM(sample_count)  AS sample_count
FROM sensor_hourly
GROUP BY device_id, bucket
WITH NO DATA;

自動更新ポリシーの設定

-- 時間別集約: 直近 3 日の範囲を 1 時間間隔で更新
SELECT add_continuous_aggregate_policy('sensor_hourly',
    start_offset  => INTERVAL '3 days',
    end_offset    => INTERVAL '1 hour',
    schedule_interval => INTERVAL '1 hour'
);

-- 日別集約: 直近 1 か月の範囲を 1 日間隔で更新
SELECT add_continuous_aggregate_policy('sensor_daily',
    start_offset  => INTERVAL '1 month',
    end_offset    => INTERVAL '1 day',
    schedule_interval => INTERVAL '1 day'
);

各パラメータの意味は次のとおり。

手動更新と確認

-- 特定の範囲を手動で更新
CALL refresh_continuous_aggregate('sensor_hourly',
    '2026-03-01', '2026-03-09');

-- 連続集約ポリシーの状態を確認
SELECT view_name, schedule_interval,
       config ->> 'start_offset' AS start_offset,
       config ->> 'end_offset'   AS end_offset
FROM timescaledb_information.continuous_aggregate_stats;

-- 連続集約クエリの性能を確認
EXPLAIN ANALYZE
SELECT device_id, bucket, avg_temp
FROM sensor_hourly
WHERE bucket >= now() - INTERVAL '7 days'
  AND device_id = 'sensor-001';

5. データ保持ポリシーと圧縮

ネイティブ圧縮の設定

TimescaleDB のネイティブ圧縮は、行ベースのデータを列ベースに変換して格納する。一般に 90% 以上のストレージ削減効果が得られる。

-- 圧縮設定: segmentby と orderby の指定が要点
ALTER TABLE sensor_data SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'device_id',
    timescaledb.compress_orderby = 'time DESC'
);

-- 7 日以上経過したチャンクを自動圧縮するポリシー
SELECT add_compression_policy('sensor_data', INTERVAL '7 days');

-- 圧縮効果の確認
SELECT
    pg_size_pretty(before_compression_total_bytes) AS before,
    pg_size_pretty(after_compression_total_bytes)  AS after,
    round(
        (1 - after_compression_total_bytes::numeric
             / before_compression_total_bytes) * 100, 1
    ) AS compression_ratio_pct
FROM hypertable_compression_stats('sensor_data');

segmentby カラムの選び方: カーディナリティが低すぎると (たとえば 3 個のステータス値) 圧縮効率が落ち、高すぎるとセグメント数が過剰になる。一般には数百から数万程度の一意値を持つカラムが適している。

データ保持ポリシー (Retention Policy)

-- 90 日以上経過した生データを自動削除
SELECT add_retention_policy('sensor_data', INTERVAL '90 days');

-- 連続集約にはより長い保持期間を設定できる
SELECT add_retention_policy('sensor_hourly', INTERVAL '1 year');
SELECT add_retention_policy('sensor_daily', INTERVAL '5 years');

-- ポリシーの実行状態を確認
SELECT application_name, schedule_interval,
       last_run_status, last_run_duration,
       next_start
FROM timescaledb_information.jobs
WHERE application_name LIKE '%retention%'
   OR application_name LIKE '%compress%';

ダウンサンプリングのパイプラインパターン

実務で最も多く使われるのは、生データ -> 連続集約 -> 圧縮 -> 削除 という段階的なデータライフサイクル管理のパターンだ。

[生データ]  --> [7 日後に圧縮] --> [90 日後に削除]
      |
      +-- [時間別の連続集約] --> [1 年後に削除]
              |
              +-- [日別の連続集約] --> [5 年後に削除]

こう構成すれば、直近のデータは秒単位の解像度で照会でき、過去のデータは集約された形で長期保存される。ストレージコストは最大 95% 以上削減できる。


6. インデックス戦略とクエリ最適化

基本のインデックス戦略

ハイパーテーブルを作成すると、時間カラムに対する B-tree インデックスが自動的に作られる。追加のインデックスはクエリパターンに合わせて設計する必要がある。

-- デバイス別の時間範囲照会が頻繁な場合
CREATE INDEX idx_sensor_device_time
ON sensor_data (device_id, time DESC);

-- 特定条件の部分インデックス (NULL でない値のみ)
CREATE INDEX idx_sensor_battery_low
ON sensor_data (device_id, time DESC)
WHERE battery < 20.0;

-- インデックス効率の確認
SELECT indexrelname, idx_scan, idx_tup_read, idx_tup_fetch,
       pg_size_pretty(pg_relation_size(indexrelid)) AS idx_size
FROM pg_stat_user_indexes
WHERE schemaname = '_timescaledb_internal'
ORDER BY idx_scan DESC
LIMIT 20;

クエリ最適化のコツ

-- GOOD: 時間範囲を明示してチャンク除外を活かす
SELECT device_id, AVG(temperature)
FROM sensor_data
WHERE time >= now() - INTERVAL '1 hour'
GROUP BY device_id;

-- BAD: 時間条件がなければ全チャンクをスキャンする
SELECT device_id, AVG(temperature)
FROM sensor_data
GROUP BY device_id;

-- GOOD: 連続集約を活用したダッシュボードクエリ
SELECT device_id, bucket, avg_temp
FROM sensor_hourly
WHERE bucket >= now() - INTERVAL '24 hours'
ORDER BY bucket DESC;

-- クエリ性能の分析
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM sensor_data
WHERE time >= now() - INTERVAL '6 hours'
  AND device_id = 'sensor-042';

中心となる最適化の原則をまとめると次のとおり。


7. TimescaleDB と InfluxDB と ClickHouse の比較

時系列データベースを選ぶときに最もよく比較される 3 つのシステムを整理する。

項目TimescaleDBInfluxDBClickHouse
基盤PostgreSQL 拡張専用エンジン (Go)専用エンジン (C++)
クエリ言語標準 SQLFlux / InfluxQLSQL (非標準拡張)
ストレージモデル行ベース + 列圧縮TSM (Time-Structured Merge)列ベース (MergeTree)
JOIN のサポート完全な SQL JOIN限定的対応 (コストは大きい)
ACID トランザクション完全対応非対応限定的
INSERT 性能約 100万 rows/sec約 100万 rows/sec約 400万 rows/sec
圧縮率10~20 倍50~100 倍20~50 倍
ディスク使用量相対的に大きい非常に効率的効率的
エコシステムPostgreSQL の全エコシステム専用の Telegraf/Grafana独立したエコシステム
学習コスト低い (SQL の知識を活用)中程度 (Flux の学習)中程度 (非標準 SQL)
高可用性PostgreSQL のレプリケーションEnterprise のみクラスタネイティブクラスタ

選択基準

実務では 1 つだけを選ぶより、データのライフサイクルに応じて組み合わせることが多い。たとえば InfluxDB でリアルタイム収集と通知を処理し、TimescaleDB でトランザクションが必要な制御システムを運用し、ClickHouse で長期の分析クエリを実行するパイプライン構成が考えられる。


8. 実践的な監視データパイプラインの構築

Telegraf で収集したサーバーメトリクスを TimescaleDB に保存し、Grafana で可視化する全体パイプラインの例だ。

Telegraf の設定

# telegraf.conf
[agent]
  interval = "10s"
  flush_interval = "10s"

[[inputs.cpu]]
  percpu = true
  totalcpu = true

[[inputs.mem]]

[[inputs.disk]]
  ignore_fs = ["tmpfs", "devtmpfs"]

[[inputs.net]]

[[outputs.postgresql]]
  connection = "host=localhost port=5432 user=telegraf dbname=metrics sslmode=disable"
  create_templates = [
    "CREATE TABLE IF NOT EXISTS {TABLE}({COLUMNS})",
    "SELECT create_hypertable('{TABLE}', by_range('time'), if_not_exists => true)",
  ]
  add_column_templates = [
    "ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS {COLUMN} {TYPE}",
  ]
  tag_table_suffix = "_tag"

スキーマ設計の例

-- サーバーメトリクスのテーブル
CREATE TABLE server_metrics (
    time        TIMESTAMPTZ NOT NULL,
    host        TEXT        NOT NULL,
    region      TEXT,
    cpu_usage   DOUBLE PRECISION,
    mem_usage   DOUBLE PRECISION,
    disk_usage  DOUBLE PRECISION,
    net_in      BIGINT,
    net_out     BIGINT
);

SELECT create_hypertable('server_metrics', by_range('time', INTERVAL '1 day'));

-- 圧縮の設定
ALTER TABLE server_metrics SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'host',
    timescaledb.compress_orderby = 'time DESC'
);

-- ポリシーの一括設定
SELECT add_compression_policy('server_metrics', INTERVAL '3 days');
SELECT add_retention_policy('server_metrics', INTERVAL '30 days');

-- 連続集約: 5 分単位
CREATE MATERIALIZED VIEW server_metrics_5m
WITH (timescaledb.continuous) AS
SELECT
    host,
    time_bucket('5 minutes', time) AS bucket,
    AVG(cpu_usage)  AS avg_cpu,
    MAX(cpu_usage)  AS max_cpu,
    AVG(mem_usage)  AS avg_mem,
    MAX(mem_usage)  AS max_mem,
    AVG(disk_usage) AS avg_disk
FROM server_metrics
GROUP BY host, bucket
WITH NO DATA;

SELECT add_continuous_aggregate_policy('server_metrics_5m',
    start_offset    => INTERVAL '7 days',
    end_offset      => INTERVAL '5 minutes',
    schedule_interval => INTERVAL '5 minutes'
);

-- 連続集約: 1 時間単位 (階層型)
CREATE MATERIALIZED VIEW server_metrics_1h
WITH (timescaledb.continuous) AS
SELECT
    host,
    time_bucket('1 hour', bucket) AS bucket,
    AVG(avg_cpu)  AS avg_cpu,
    MAX(max_cpu)  AS max_cpu,
    AVG(avg_mem)  AS avg_mem,
    MAX(max_mem)  AS max_mem,
    AVG(avg_disk) AS avg_disk
FROM server_metrics_5m
GROUP BY host, bucket
WITH NO DATA;

SELECT add_continuous_aggregate_policy('server_metrics_1h',
    start_offset    => INTERVAL '30 days',
    end_offset      => INTERVAL '1 hour',
    schedule_interval => INTERVAL '1 hour'
);

9. トラブルシューティングと運用上の注意点

チャンクのロック (Lock) に関する問題

チャンクの削除 (drop_chunks) や圧縮では、そのチャンクに対する排他ロック (exclusive lock) が必要になる。別のセッションがそのチャンクを参照していると、タイムアウトして失敗する。

-- チャンクに対するロックを確認
SELECT pid, mode, granted, relation::regclass
FROM pg_locks
WHERE relation IN (
    SELECT format('%I.%I', chunk_schema, chunk_name)::regclass
    FROM timescaledb_information.chunks
    WHERE hypertable_name = 'sensor_data'
      AND NOT is_compressed
);

-- 問題となるセッションを終了 (注意: 進行中の作業はロールバックされる)
SELECT pg_terminate_backend(<pid>);

圧縮失敗への対応

大きなチャンクを圧縮するとき、temp_file_limit または maintenance_work_mem の不足で失敗することがある。

-- 一時的に制限を緩めてから手動で圧縮
SET temp_file_limit = '10GB';
SET maintenance_work_mem = '2GB';
SELECT compress_chunk('<chunk_name>');

バックグラウンドワーカー (Background Worker) の停止

予約されたポリシー (圧縮、保持、連続集約の更新) が実行されないことがある。

-- バックグラウンドワーカーの再起動
SELECT timescaledb_pre_restore();
SELECT timescaledb_post_restore();

-- 実行に失敗したジョブを確認
SELECT job_id, application_name, last_run_status,
       last_run_started_at, last_run_duration,
       total_failures
FROM timescaledb_information.job_stats
WHERE last_run_status = 'Failed';

連続集約の更新遅延

連続集約の end_offset より新しいデータは集約に含まれない。ダッシュボードで最新データが欠けているように見える場合は、この設定を確認する。


10. 失敗事例と復旧手順

事例 1: チャンク間隔を小さくしすぎたことによるメタデータの急増

問題: チャンク間隔を 1 分に設定したため、数十万個のチャンクが生成された。クエリプランナがメタデータを処理するのに数秒かかり、すべてのクエリが遅くなった。

復旧:

-- チャンク間隔を適切な値に変更
SELECT set_chunk_time_interval('sensor_data', INTERVAL '1 day');

-- 古い小さなチャンクを手動で整理 (保持ポリシーを通じて)
SELECT drop_chunks('sensor_data', older_than => INTERVAL '30 days');

予防: チャンク数が 1,000 個を超えないよう監視し、1 日あたりの INSERT ボリュームに合った間隔を設定する。

事例 2: 圧縮後の UPDATE/DELETE の試行

問題: すでに圧縮されたチャンクに UPDATE または DELETE を実行すると、エラーになるか非常に遅くなる。

復旧:

-- 圧縮を解除してから修正
SELECT decompress_chunk('<chunk_name>');
-- UPDATE または DELETE を実行
UPDATE sensor_data SET temperature = NULL
WHERE time = '2026-03-01 12:00:00' AND device_id = 'sensor-bad';
-- 再び圧縮
SELECT compress_chunk('<chunk_name>');

予防: 圧縮の対象は変更が完了したデータであるべきだ。compress_after の間隔を十分に長く設定する。

事例 3: ディスク容量不足による INSERT 失敗

問題: 保持ポリシーが実行される前にディスクが満杯になり、新しいデータの INSERT が失敗した。

復旧:

-- 緊急のチャンク削除
SELECT drop_chunks('sensor_data', older_than => INTERVAL '7 days');

-- 未圧縮の古いチャンクを直ちに圧縮
SELECT compress_chunk(c.chunk_name)
FROM timescaledb_information.chunks c
WHERE c.hypertable_name = 'sensor_data'
  AND NOT c.is_compressed
  AND c.range_end < now() - INTERVAL '2 days'
ORDER BY c.range_start ASC;

予防: ディスク使用率 80% の通知を設定し、保持ポリシーの周期をディスクの増加率に合わせて調整する。

バックアップと復旧

# pg_dump を使った論理バックアップ
pg_dump -Fc -f backup.dump mydb

# 復旧前に pre_restore の呼び出しが必須
psql -d mydb -c "SELECT timescaledb_pre_restore();"
pg_restore -d mydb backup.dump
psql -d mydb -c "SELECT timescaledb_post_restore();"

11. 運用チェックリスト

TimescaleDB をプロダクションで運用する際に必ず確認すべき項目をまとめる。

設計段階

ポリシー設定

モニタリング

バックアップと復旧


12. まとめ

TimescaleDB は、PostgreSQL の信頼性とエコシステムを保ちながら時系列ワークロードに特化した性能を提供する、実用的な選択肢だ。要点は 3 つある。

  1. 適切なチャンク間隔の設定: アクティブなインデックスがメモリに常駐できるよう、1 日あたりのデータ量に合わせて調整する。
  2. 連続集約と圧縮の組み合わせ: 生データは圧縮してから削除し、集約データは長期保存することで、ストレージとクエリ性能を同時に最適化する。
  3. ポリシーによる自動化: 圧縮、保持、連続集約の更新をすべてポリシーで自動化し、運用負担を減らす。

InfluxDB や ClickHouse との違いは、SQL 互換性と ACID トランザクションのサポートだ。既存の PostgreSQL 環境があり、リレーショナルデータとの JOIN が必要な環境なら、TimescaleDB が最も自然な選択になる。


参考資料

コメント

まだコメントはありません。

ログインするとコメントできます