本文是 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 專屬機制:

  1. Hypertable:對 time column 自動 partition、應用層看是一張表
  2. Continuous aggregate:incremental refresh 的 materialized view
  3. Compression:對舊 chunk 壓縮(columnar-like format)

跟專業 time-series DB(InfluxDB / Prometheus / VictoriaMetrics)對比、TimescaleDB 的賣點不是「最快」而是「PG ecosystem 一致」:

維度TimescaleDBInfluxDBPrometheus
Query 語言標準 SQLInfluxQL / FluxPromQL
寫入效能中(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 sourceApache 2.0(部分功能 TSL license)MITApache 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_sizeshared_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 metrics90-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.x13, 14, 15, 16最後支援 PG 13 的 minor
2.16+14, 15, 16PG 13 drop
2.17+14, 15, 16, 17PG 17 加入(需 17.2+ binary 對齊)
2.18+14, 15, 16, 17PG 17 完整支援
2.23+14, 15, 16, 17, 18PG 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 hypertablePG declarative partitioning
自動建 chunk否(需手動或 pg_partman)
Chunk pruning自動自動(需 partition key)
Retention 內建否(pg_partman 或自寫 cron)
Compression內建 columnar
Continuous aggregate內建否(自寫 incremental refresh)
跨 chunk index統一 managementPer-partition index
Cardinality limit10000+ chunk OK1000+ 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

相關連結

下一步