正在加载,请稍候…

TimescaleDB: Time Series Data in PostgreSQL

Leverage TimescaleDB for IoT and metrics data — hypertables, continuous aggregates, compression policies, data retention, and comparing with InfluxDB.

Why TimescaleDB?

TimescaleDB is PostgreSQL with time-series superpowers. You get familiar SQL, existing tooling, and 100x query performance on time-series data.

Setup

-- Enable extension
CREATE EXTENSION IF NOT EXISTS timescaledb;

-- Create regular table
CREATE TABLE metrics (
  time        TIMESTAMPTZ NOT NULL,
  device_id   TEXT NOT NULL,
  metric_name TEXT NOT NULL,
  value       DOUBLE PRECISION
);

-- Convert to hypertable (auto-partitioned by time)
SELECT create_hypertable('metrics', 'time',
  chunk_time_interval => INTERVAL '1 day',
  if_not_exists => TRUE
);

-- Add composite index for common queries
CREATE INDEX idx_metrics_device_time ON metrics (device_id, time DESC);

Insert and Query

-- Insert (same as regular PostgreSQL)
INSERT INTO metrics (time, device_id, metric_name, value)
VALUES
  (NOW(), 'sensor-001', 'temperature', 23.5),
  (NOW(), 'sensor-001', 'humidity', 65.2),
  (NOW(), 'sensor-002', 'temperature', 21.1);

-- Time-bucket aggregation
SELECT
  time_bucket('1 hour', time) AS hour,
  device_id,
  AVG(value) AS avg_temp,
  MAX(value) AS max_temp,
  MIN(value) AS min_temp
FROM metrics
WHERE metric_name = 'temperature'
  AND time > NOW() - INTERVAL '24 hours'
GROUP BY hour, device_id
ORDER BY hour DESC, device_id;

-- Gap filling (insert null for missing intervals)
SELECT
  time_bucket_gapfill('1 hour', time) AS hour,
  device_id,
  LOCF(AVG(value)) AS temperature  -- Last observation carried forward
FROM metrics
WHERE metric_name = 'temperature'
  AND time > NOW() - INTERVAL '7 days'
  AND device_id = 'sensor-001'
GROUP BY hour, device_id
ORDER BY hour;

Continuous Aggregates

-- Pre-compute hourly averages (auto-refreshed)
CREATE MATERIALIZED VIEW metrics_hourly
WITH (timescaledb.continuous) AS
SELECT
  time_bucket('1 hour', time) AS hour,
  device_id,
  metric_name,
  AVG(value) AS avg_value,
  MAX(value) AS max_value,
  MIN(value) AS min_value,
  COUNT(*) AS sample_count
FROM metrics
GROUP BY hour, device_id, metric_name
WITH NO DATA;

-- Set refresh policy
SELECT add_continuous_aggregate_policy('metrics_hourly',
  start_offset => INTERVAL '3 hours',
  end_offset   => INTERVAL '1 hour',
  schedule_interval => INTERVAL '30 minutes'
);

-- Query the view (super fast!)
SELECT hour, avg_value, max_value
FROM metrics_hourly
WHERE device_id = 'sensor-001'
  AND metric_name = 'temperature'
  AND hour > NOW() - INTERVAL '7 days'
ORDER BY hour DESC;

Compression Policy

-- Compress chunks older than 7 days (typically 90%+ compression)
SELECT add_compression_policy('metrics',
  compress_after => INTERVAL '7 days'
);

-- Monitor compression
SELECT
  chunk_name,
  before_compression_total_bytes,
  after_compression_total_bytes,
  round(after_compression_total_bytes::numeric / before_compression_total_bytes * 100, 1) AS pct
FROM chunk_compression_stats('metrics')
ORDER BY chunk_name;

Data Retention

-- Auto-drop data older than 90 days
SELECT add_retention_policy('metrics', drop_after => INTERVAL '90 days');

Node.js with TimescaleDB

import { Pool } from 'pg'

const pool = new Pool({ connectionString: process.env.DATABASE_URL })

async function insertMetrics(readings: MetricReading[]) {
  const values = readings.map(r => `('${r.time}', '${r.deviceId}', '${r.metric}', ${r.value})`).join(',')
  await pool.query(`INSERT INTO metrics (time, device_id, metric_name, value) VALUES ${values}`)
}

async function getDeviceMetrics(deviceId: string, hours: number) {
  const result = await pool.query(`
    SELECT
      time_bucket('5 minutes', time) AS bucket,
      AVG(value) AS avg_value,
      MAX(value) AS max_value
    FROM metrics
    WHERE device_id = $1
      AND time > NOW() - $2 * INTERVAL '1 hour'
      AND metric_name = 'temperature'
    GROUP BY bucket
    ORDER BY bucket DESC
  `, [deviceId, hours])
  return result.rows
}

TimescaleDB vs InfluxDB

Feature TimescaleDB InfluxDB
Query language SQL InfluxQL/Flux
Existing PG tooling Yes No
Joins Full SQL joins Limited
Compression ~90% ~95%
Writes/sec ~1M ~1.5M
Ecosystem PostgreSQL Purpose-built