タイムシリーズDBの内部:ダウンサンプリングとロールアップ
メトリクスが膨れても安く速く保つ時系列DBの裏側が分かります。追記専用ストレージ・時刻パーティション・ダウンサンプリングと delta-of-delta 圧縮の原理を押さえられます。
- 1.時系列DBは時刻順の追記が大半・更新がほぼ無いという特性を前提に、時刻でパーティション分割した追記専用ストレージを使う。古い窓は丸ごと不変化でき、保持期限切れは窓ごと一括破棄できる。
- 2.ダウンサンプリング(ロールアップ)は生データを一定間隔の集約(平均・最大・カウント等)へ事前計算し、低解像度の派生系列を作る。保持ポリシーで生データは短く、集約は長く保つことで容量と長期クエリ性能を両立する。
- 3.圧縮はタイムスタンプに delta-of-delta、値に XOR(Gorilla 系)を使う。等間隔なら二次差分がほぼ0、値も近い浮動小数で先頭ビットが揃うため、1サンプルを数ビットまで縮められる。
時系列ワークロードは何が特殊か
時系列データベース(TSDB: Time-Series Database)は、サーバーメトリクス・IoT センサー・金融ティック・APM トレースのように 時刻付きの測定値が大量に書き込まれるワークロードに特化したストレージです。汎用 RDB と同じ表として扱うこともできますが、TSDB が別物として設計されるのは、時系列ワークロードに次の偏りがあるからです。
- 書き込みは時刻順の追記がほぼ全て。過去のレコードを後から書き換える更新はまれで、削除も「古い期間を丸ごと捨てる」形が中心。
- 同じ系列(series)が高頻度で繰り返す。
cpu_usage{host=web01}のような同一識別子に、タイムスタンプと値だけが次々付け足される。 - 読み取りは時間範囲スキャン + 集約が主役。「直近1時間の P99」「過去30日の日次平均」のように、ある期間を読んで平均・最大・カウントへ畳む。点検索(特定1行の取得)は少ない。
この「追記専用に近く、系列ごとの時系列スキャンと集約が主」という性質を最大限に使い切ることが、TSDB の内部設計の出発点です。
TSDB の基本単位は行ではなく **系列(series)**です。系列とはメトリクス名とラベルの組(例 http_requests_total{method=GET,host=web01})で一意に決まる時系列で、その実体は (timestamp, value) ペアの列です。ラベルの組み合わせ数を カーディナリティと呼び、これが TSDB の容量・インデックスサイズを支配します。高カーディナリティ(無数のラベル値)は TSDB が最も苦手とする負荷です。
時刻パーティションと追記専用ストレージ
TSDB はデータを 時刻でパーティション分割します。Prometheus の block、InfluxDB の shard、TimescaleDB の chunk など名称は様々ですが、いずれも「ある時間窓(例: 2時間ぶん、1日ぶん)」を1つの不変なまとまりとして扱う点が共通です。これが効く理由は明確です。
- 古い窓は不変化(immutable)できる。書き込みは常に最新の窓に集中するため、過去の窓はもう変化しない。不変ならロック不要で読め、強く圧縮でき、ファイルキャッシュも安定する。
- 範囲クエリで窓を枝刈りできる。「先週分」を読むなら、時刻範囲が重ならない窓は開かずに飛ばせる。これは時刻という単一軸でデータが物理的に並んでいるから成立します。一般的なパーティショニングのレンジ分割を、時刻軸に特化させたものと考えると理解しやすいです。
- 保持期限切れを安価に消せる。期限を過ぎた窓は ファイルごと unlink するだけで削除完了。行を1つずつ DELETE する必要がない。
書き込みパスは LSM 的です。直近の書き込みはメモリ上の可変バッファ(Prometheus の head block 等)に追記し、WAL で永続性を担保しつつ、一定時間・サイズで不変ブロックへ書き出します。LSM のコンパクションを時刻窓に特化させた戦略が、まさに時系列向けに使われます。詳しくは LSMコンパクション戦略 の FIFO / TWCS(TimeWindowCompactionStrategy)を参照してください。古い窓を「マージせず丸ごと破棄」できるのは、時刻パーティションと追記専用性の合わせ技です。
追記専用の前提は「新しいデータほど後に来る」です。ネットワーク遅延や再送で古いタイムスタンプが遅れて届くと、すでに閉じた窓へ後挿入が必要になり、ブロックの再構成や別系統の受け皿が要ります。TSDB が許容する遅延到着の窓幅(lookback / out-of-order window)に上限があるのはこのためです。設計上は「ほぼ時刻順」を期待して最適化されている点に注意します。
delta-of-delta と XOR:1サンプルを数ビットに
時系列の圧縮は、汎用圧縮の前段に 値の性質を使った軽量符号化を置くのが定石です。鍵は2つの規則性です。
タイムスタンプは等間隔に近い。10秒間隔で取得するメトリクスなら、隣接タイムスタンプの差分(delta)はほぼ一定の 10000(ミリ秒)です。差分を取れば値域は縮みますが、さらに **差分の差分(delta-of-delta、二次差分)**を取ると、等間隔の区間では 0 がずっと続きます。0 の連続はごく短いビット列で表現できるため、1タイムスタンプあたり実質1〜数ビットまで落ちます。これは隣接差分を持つデルタ符号化を時刻列向けにもう一段重ねた形です。
生のタイムスタンプ(ms): 1000 11000 21000 31000 41500
一次差分(delta): +10000 +10000 +10000 +10500
二次差分(delta-of-delta): 0 0 +500
└─ 等間隔の間はずっと 0 → 数ビットで表現
値は隣接サンプルが近い。CPU 使用率や温度は、ある時点と次の時点で大きく跳ねません。Gorilla(Facebook の論文に由来する方式で、Prometheus TSDB などが採用)は、隣り合う浮動小数値を XOR し、その結果の 先頭/末尾の連続ゼロビット数だけを記録します。値が近ければ XOR 結果は上位ビットが揃って0が多く、保持すべき「意味のあるビット」はごくわずか。値が変わらなければ XOR は完全に0で、1ビットで「前と同じ」を表せます。
| 対象 | 符号化 | 効く理由 |
|---|---|---|
| タイムスタンプ | delta-of-delta | 等間隔なら二次差分が 0 の連続になり、ほぼ消える |
| 浮動小数の値 | 直前値との XOR + 有効ビットのみ | 隣接値が近いと上位ビットが一致し残差が小さい |
| 整数カウンタ | delta(差分) | 単調増加で差分が小さく一定値付近に収まる |
これらの符号化で1サンプルが平均数バイトから 1〜2バイト前後まで縮むため、TSDB は同じディスクで桁違いの本数を保持できます。圧縮はブロック(時刻窓)内で閉じて行われるので、復号も窓単位で完結し、不変ブロックの性質と噛み合います。
タイムスタンプ列と値列は分布が全く違う(片や等間隔、片や近接値)ため、同じ符号化を混在させると効率が落ちます。TSDB は系列ごとにタイムスタンプ列・値列を分離して保持し、各列に最適な符号化を当てます。これは行を丸ごと並べるより列単位で符号化する発想で、考え方は列指向ストレージと同根です。
ダウンサンプリング(ロールアップ)と保持ポリシー
生データ(raw, 例: 10秒粒度)をそのまま長期間保持すると、容量も長期クエリのスキャン量も膨大になります。そこで **ダウンサンプリング(downsampling)= ロールアップ(rollup)**を行います。生データを一定間隔(例: 5分、1時間、1日)の 集約へ事前計算し、低解像度の派生系列を生成しておく仕組みです。
ロールアップで何を保持するかが要点です。後段で別の集約を正しく再計算できるよう、結合可能な集約値を持ちます。
| 保持する集約 | 用途 | 再集約できるか |
|---|---|---|
| count / sum | 平均は sum/count で再計算。上位窓へ合算できる | 可(加法的) |
| min / max | 外れ値・ピーク把握。上位窓は min/max を取り直すだけ | 可(結合的) |
| 平均(avg)だけ | 見かけは便利だが、count が無いと上位窓へ再平均できない | 不可(要 count) |
たとえば「1時間平均」をさらに「日平均」へ畳むには、各時間の avg を単純平均しては誤ります(各時間のサンプル数が違うため)。sum と count を保持しておけば、日次の sum(sum)/sum(count) で正しい加重平均になります。パーセンタイル(P99 等)が単純には再集約できず、ヒストグラムや t-digest などのスケッチを保持する必要があるのも同じ理由です。集約は「上位の窓へ合成できる形」で持つのが原則です。
ロールアップは生データから派生する事前計算という点でマテリアライズドビューの発想に近く、TSDB はこれを時刻窓に沿って自動・継続的に更新します。
**保持ポリシー(retention policy)**はこれと一体で機能します。解像度ごとに別の保持期間を設定します。
保持ポリシーの典型例:
raw (10秒粒度) → 7日間だけ保持 (細かいが短命)
5分ロールアップ → 90日間保持
1時間ロールアップ → 2年間保持 (粗いが長命)
クエリ範囲に応じて最適解像度を自動選択:
直近1時間のグラフ → raw を読む
過去1年のトレンド → 1時間ロールアップを読む(スキャン量が桁違いに少ない)
これにより、短期は高解像度・長期は低解像度という階層ができ、ディスク使用量を抑えつつ長期クエリも軽くなります。期限切れの解像度は時刻窓ごと丸ごと破棄できるので、削除コストもほぼゼロです。
1時間平均だけを残して raw を消すと、その1時間内の瞬間スパイクや分布は二度と復元できません。ダウンサンプリングは平滑化であり、外れ値検知やインシデント後の精密分析には raw が要ります。保持ポリシーは「どの粒度をどこまで残せば運用要件を満たすか」の設計判断であり、安易に raw を短くするとデバッグ能力を失います。
時刻パーティション = 古い窓の不変化と窓ごと一括削除を可能にする、delta-of-delta = 等間隔タイムスタンプの二次差分が0連続になるのを使う、Gorilla の XOR = 隣接浮動小数値の近さを使う、ロールアップは sum/count を保持して上位窓へ再集約する(avg 単独はダメ)、保持ポリシーは解像度ごとに保持期間を分ける。この対応関係をセットで押さえると応用が利きます。
まとめ
時系列DBは「時刻順の追記が大半・系列スキャンと集約が主」という偏りを前提に、時刻でパーティション分割した追記専用ストレージを採ります。古い窓は不変化でき、保持期限切れは窓ごと一括破棄で安く消せます。圧縮はタイムスタンプに delta-of-delta、浮動小数値に **XOR(Gorilla 系)**を当て、規則性を使って1サンプルを数ビットへ縮めます。**ダウンサンプリング(ロールアップ)**は生データを集約へ事前計算し、sum/count のような再集約可能な形で保持。保持ポリシーで解像度ごとに保持期間を分け、短期は高解像度・長期は低解像度として容量と長期クエリ性能を両立させます。
データベース Article
タイムシリーズDBの内部:ダウンサンプリングとロールアップを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
時系列DB
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
ダウンサンプリング(ロールアップ)は生データを一定間隔の集約(平均・最大・カウント等)へ事前計算し、低解像度の派生系列を作る。保持ポリシーで生データは短く、集約は長く保つことで容量と長期クエリ性能を両立する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「時系列DB / ダウンサンプリング」に近いか確認する。
- 強みである「時系列DBは時刻順の追記が大半・更新がほぼ無いという特性を前提に、時刻でパーティション分割した追記専用ストレージを使う。古い窓は丸ごと不変化でき、保持期限切れは窓ごと一括破棄できる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。