TimescaleDB Deep Dive:Hypertable / Continuous Aggregate / Compression 把 PG 變 Time-Series DB
本文是 PostgreSQL overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 TimescaleDB extension — 用 PG 解 time-series workload 的路徑、跟 extension-ecosystem 是 單一 extension 細節 vs ecosystem 全景 的關係。
TimescaleDB 是 PG 的 Time-Series Specialization
TimescaleDB 不是獨立 DB、是 PG extension:
1CREATE EXTENSION timescaledb;加完後、PG 多三個 time-series 專屬機制:
- Hypertable:對 time column 自動 partition、應用層看是一張表
- Continuous aggregate:incremental refresh 的 materialized view
- Compression:對舊 chunk 壓縮(columnar-like format)
跟專業 time-series DB(InfluxDB / Prometheus / VictoriaMetrics)對比、TimescaleDB 的賣點不是「最快」而是「PG ecosystem 一致」:
| 維度 | TimescaleDB | InfluxDB | Prometheus |
|---|---|---|---|
| Query 語言 | 標準 SQL | InfluxQL / Flux | PromQL |
| 寫入效能 | 中(10-100K rows/s) | 高(500K+ rows/s) | 中(pull-based scrape) |
| 壓縮 | 90%+(columnar compression) | 高 | 高 |
| Join | 完整 SQL join | 弱 | 不支援 |
| 跟既有 PG schema | 同一個 DB、可 join | 獨立 | 獨立 |
| 生態 | 完整 PG ecosystem | 自家 ecosystem | 自家 ecosystem |
| Open source | Apache 2.0(部分功能 TSL license) | MIT | Apache 2.0 |
何時選 TimescaleDB:
- Application 已用 PG、不想多管一套 time-series DB
- 需要 join time-series 跟 application 表(user / device metadata)
- 不需 InfluxDB 級寫入速度(< 100K rows/s)
- Team SQL 熟、PromQL / Flux 學習成本不想付
何時選 InfluxDB / Prometheus(不選 TimescaleDB):
- High-cardinality metric(10M+ unique series)— TSDB-purpose-built engine 在 cardinality 跟 retention 上比 hypertable 高效
- Pull-based scrape model(Prometheus)跟 alerting / Grafana 生態深整合
- PromQL operator(
rate()/histogram_quantile())對 metric query 比 SQL 直覺 - TSL license 不能接受(TimescaleDB 部分功能在 Timescale License、不是純 Apache 2.0)
- Operational team 已熟 InfluxDB / Prometheus、不想多學 PG 維運
Hypertable:自動 Time-based Partitioning
普通 PG 表變 hypertable:
1CREATE TABLE sensor_data (
2 time TIMESTAMPTZ NOT NULL,
3 sensor_id INTEGER NOT NULL,
4 temperature DOUBLE PRECISION,
5 humidity DOUBLE PRECISION
6);
7
8-- 變 hypertable、按 time 自動 partition
9SELECT create_hypertable('sensor_data', 'time');Hypertable 機制:
- 後台自動拆 chunk(child partition)by time interval(預設 7 天)
- Application 看到的是
sensor_data一張表、實際資料分散在_timescaledb_internal._hyper_*_chunk表 - Query 自動 chunk pruning(只掃命中時間範圍的 chunk)
Chunk interval 選擇很關鍵:
| Chunk interval | 適用 | 問題 |
|---|---|---|
| 1 小時 | 高頻 metrics(每秒 100+ row) | Chunk 太多、catalog 膨脹 |
| 1 天 | 中高頻(每秒 10-100 row) | OK |
| 7 天(預設) | 中頻(每分鐘 row) | OK |
| 30 天 | 低頻(每小時 row) | OK |
通用原則:每個 chunk 25% RAM、超過退化 disk IO。Production 監控 chunk_size 跟 shared_buffers ratio 自動調。
Multi-dimensional hypertable(time + space partition):
1-- 按 time + device_id 雙維 partition
2SELECT create_hypertable('sensor_data', 'time',
3 partitioning_column => 'sensor_id',
4 number_partitions => 16
5);適用 sensor 數 1000+ 的 IoT workload、單 chunk 太大時用 space partition 拆。
Continuous Aggregate(CAGG):Incremental Materialized View
普通 PG materialized view 是 全量重算、TimescaleDB CAGG 是 incremental refresh:
1-- 1 小時粒度聚合
2CREATE MATERIALIZED VIEW sensor_hourly
3WITH (timescaledb.continuous) AS
4SELECT
5 time_bucket('1 hour', time) AS hour,
6 sensor_id,
7 avg(temperature) AS avg_temp,
8 max(temperature) AS max_temp,
9 min(temperature) AS min_temp,
10 count(*) AS sample_count
11FROM sensor_data
12GROUP BY hour, sensor_id;
13
14-- 加 refresh policy(每 30 分鐘 refresh 過去 1 天)
15SELECT add_continuous_aggregate_policy('sensor_hourly',
16 start_offset => INTERVAL '1 day',
17 end_offset => INTERVAL '30 minutes',
18 schedule_interval => INTERVAL '30 minutes'
19);CAGG 機制:
- 記錄哪些 time bucket 已 materialize、哪些 stale
- Refresh 時只重算 stale bucket、不全量
- Query CAGG 自動 fallback 到原 hypertable 補最新資料(real-time aggregation)
CAGG vs 普通 MV 對比:
| 維度 | TimescaleDB CAGG | 普通 PG MV |
|---|---|---|
| Refresh 模式 | Incremental | 全量重算 |
| Refresh 時間 | 秒級 | 表大時數十分鐘 |
| Real-time fallback | 自動補最新 | 不支援、需手動 union |
| Storage | 多一份 aggregated | 多一份 aggregated |
| Policy | 內建排程 | 需 pg_cron / 外部排程 |
CAGG hierarchy(多層聚合):
1-- 從 1 hour CAGG 再聚合到 1 day
2CREATE MATERIALIZED VIEW sensor_daily
3WITH (timescaledb.continuous) AS
4SELECT
5 time_bucket('1 day', hour) AS day,
6 sensor_id,
7 avg(avg_temp) AS daily_avg
8FROM sensor_hourly
9GROUP BY day, sensor_id;Application query 不同時間範圍時自動命中對應粒度、不必每次掃原始資料。
Compression:把舊 Chunk 壓 90%+
舊 chunk 可以開啟 compression:
1-- 開啟 compression(必須先設定 segment by)
2ALTER TABLE sensor_data SET (
3 timescaledb.compress,
4 timescaledb.compress_segmentby = 'sensor_id',
5 timescaledb.compress_orderby = 'time DESC'
6);
7
8-- 自動壓縮 policy:7 天前 chunk 壓
9SELECT add_compression_policy('sensor_data', INTERVAL '7 days');Compression 機制:
- 把 chunk 內 row 按
segmentby分組 - 每組內按
orderby排序後、把每 column 變成 columnar array - 對 array 用 type-specific 壓縮(Gorilla for float / delta-of-delta for timestamp / dictionary for string)
實際壓縮率:
| Workload | 壓縮率 |
|---|---|
| IoT sensor(重複值多) | 95-98% |
| Application metrics | 90-95% |
| Trade tick(隨機浮點) | 70-85% |
| Log line(高 cardinality string) | 50-70% |
Compression 限制(重要):
- 壓縮後 chunk 不能 UPDATE / DELETE 單 row(要先 decompress)
- 壓縮後 chunk 不能加 column(要 decompress 所有 chunk)
- 壓縮後 chunk 只能 append new row、不能改舊 row
- DDL 變更(加 column / 改 index)需 decompress
實務:compression 是 write-once cold data 的工具、active OLTP chunk 不開。
Retention Policy:自動刪舊資料
1-- 1 年前 chunk 自動刪
2SELECT add_retention_policy('sensor_data', INTERVAL '1 year');Retention drop 整個 chunk(不是 DELETE row)、O(1) 操作、不產生 bloat。
CAGG 有獨立 retention:
1-- 原始資料只留 30 天、aggregated 留 5 年
2SELECT add_retention_policy('sensor_data', INTERVAL '30 days');
3SELECT add_retention_policy('sensor_hourly', INTERVAL '5 years');這是 TimescaleDB 跟普通 PG partitioning 最大的價值差 — 普通 PG 要自己寫 cron drop partition、TimescaleDB policy 內建。
5 個 Production 踩雷
Case 1:Chunk size 不對、catalog 膨脹
情境:sensor 每秒寫 10 row、chunk_interval 設 1 小時、一年產 8760 chunk、pg_class 撐到 200 萬 row、planner 變慢。
修法:
- Chunk 數量上限 ~10000、超過 catalog overhead 出現
- 重設 chunk_interval:
SELECT set_chunk_time_interval('sensor_data', INTERVAL '1 day'); - 已存在 chunk 不會自動 merge、要靠 retention drop 自然消化
Case 2:CAGG refresh 落後 real-time
情境:CAGG refresh policy 每 1 小時跑、application 期待「即時 dashboard」、看到的數字落後 1 小時。
修法:
- 縮短
schedule_interval(5 分鐘) - 用
real-time aggregation(預設 ON、CAGG 自動 union 原始資料) - 確認
materialized_only = false(real-time aggregation 開啟)
1ALTER MATERIALIZED VIEW sensor_hourly SET (timescaledb.materialized_only = false);Case 3:Compression 後想 UPDATE
情境:發現某個歷史 row 數值錯、想 UPDATE、報錯 cannot update/delete from compressed chunk。
修法:
1-- 找到該 chunk 並 decompress
2SELECT decompress_chunk(c) FROM show_chunks('sensor_data',
3 older_than => INTERVAL '7 days') c WHERE c::text LIKE '%_5_chunk';
4
5-- UPDATE 完再 compress 回去
6UPDATE sensor_data SET temperature = 22.5 WHERE ...;
7SELECT compress_chunk(...);或設計階段就避免 — compression 用在 immutable data、有可能改的留未壓。
Case 4:Hypertable 不能加 FK 到 non-hypertable
情境:想對 sensor_data 加 FK 到 sensors 表、報錯 foreign key constraints with hypertables are not supported。
修法:
- Application 層維護 referential integrity
- 或反過來:
sensors可以 FK 到 hypertable(特定方向支援) - TimescaleDB 2.11+ 部分支援 FK from hypertable、但限制多
Case 5:TimescaleDB 跟 PG 主版本對齊
情境:PG 升級 14 → 16、TimescaleDB extension 沒對應升級、PG 啟動 fail。
TimescaleDB 跟 PG 版本對齊矩陣:
| TimescaleDB | 支援 PG version | 備註 |
|---|---|---|
| 2.11+ | 13, 14, 15 | |
| 2.13+ | 13, 14, 15, 16 | 加 PG 16 支援 |
| 2.15.x | 13, 14, 15, 16 | 最後支援 PG 13 的 minor |
| 2.16+ | 14, 15, 16 | PG 13 drop |
| 2.17+ | 14, 15, 16, 17 | PG 17 加入(需 17.2+ binary 對齊) |
| 2.18+ | 14, 15, 16, 17 | PG 17 完整支援 |
| 2.23+ | 14, 15, 16, 17, 18 | PG 18 加入 |
修法:
- 升 PG 前先升 TimescaleDB 到支援目標 PG 版本的 extension
- Production 升級順序:TimescaleDB minor upgrade → PG major upgrade → TimescaleDB final upgrade
- Cloud managed(Timescale Cloud)自動處理
跟 PG 原生 Partitioning 對比
PG 10+ 有 declarative partitioning、不一定要 TimescaleDB:
| 維度 | TimescaleDB hypertable | PG declarative partitioning |
|---|---|---|
| 自動建 chunk | 是 | 否(需手動或 pg_partman) |
| Chunk pruning | 自動 | 自動(需 partition key) |
| Retention 內建 | 是 | 否(pg_partman 或自寫 cron) |
| Compression | 內建 columnar | 否 |
| Continuous aggregate | 內建 | 否(自寫 incremental refresh) |
| 跨 chunk index | 統一 management | Per-partition index |
| Cardinality limit | 10000+ chunk OK | 1000+ partition 就慢 |
何時用原生 partitioning(不用 TimescaleDB):
- 不需要 compression / CAGG
- Partition 數 < 1000
- 已用 pg_partman 不想換
- 公司禁用 TSL license(TimescaleDB 部分功能受限)
何時用 TimescaleDB:
- 高頻 time-series(compression 必要)
- 需要 CAGG(手寫 incremental MV 成本高)
- Partition 數 > 1000
- IoT / metrics / observability workload
詳細 partitioning 機制看 declarative-partitioning。
相關連結
- extension-ecosystem:PG extension 全景
- declarative-partitioning:原生 partitioning
- jsonb-deep-dive:IoT payload 用 JSONB 儲存
- autovacuum-tuning:hypertable autovacuum 行為
- major-version-upgrade:TimescaleDB + PG 升級順序
下一步
- 看 extension-ecosystem 了解其他 PG 擴展選項
- 回 PostgreSQL overview 看全圖
#backend #database #postgresql #timescaledb #time-series #extension #deep-article