TimescaleDB

infrastructure

TimescaleDB

Definition

TimescaleDB is an open-source PostgreSQL extension that transforms PostgreSQL into a high-performance time-series database. It introduces hypertables -- automatically partitioned tables that split data by time intervals (called chunks) -- while maintaining full PostgreSQL compatibility. Applications interact with hypertables using standard SQL (INSERT, SELECT, JOIN, CTE, window functions), but the underlying storage engine is optimized for the write-heavy, time-ordered, range-query patterns that characterize time-series workloads.

The core insight behind TimescaleDB is that time-series data has different access patterns than transactional data. Writes are predominantly inserts (append-only, monotonically increasing timestamps). Reads are predominantly range scans (last hour, last day, last month). Old data is rarely modified but frequently aggregated. These patterns enable aggressive optimizations: automatic chunk-based partitioning eliminates index bloat on large tables, native columnar compression achieves 90%+ space savings on historical data, and continuous aggregates provide materialized views that auto-refresh without manual maintenance.

Because TimescaleDB is a PostgreSQL extension rather than a separate database, it inherits all of PostgreSQL's strengths: ACID transactions, advanced indexing (B-tree, GiST, GIN, BRIN), full-text search, JSON support, PostGIS geospatial queries, and the entire PostgreSQL ecosystem of tools, drivers, and operational knowledge. Applications can join time-series hypertables with regular relational tables in a single query, eliminating the data integration challenges that arise when time-series data lives in a separate specialized database.

Core Concepts

Hypertables

A hypertable is a TimescaleDB table that is automatically partitioned into chunks by time:

-- Create a regular PostgreSQL table
CREATE TABLE security_ratings (
    time        TIMESTAMPTZ NOT NULL,
    domain      TEXT NOT NULL,
    score       FLOAT NOT NULL,
    grade       CHAR(1) NOT NULL,
    details     JSONB
);

-- Convert to a hypertable (automatic time-based partitioning)
SELECT create_hypertable('security_ratings', 'time',
    chunk_time_interval => INTERVAL '1 day'
);

-- Queries work with standard SQL -- TimescaleDB optimizes internally
SELECT domain, avg(score), count(*)
FROM security_ratings
WHERE time > NOW() - INTERVAL '7 days'
GROUP BY domain
ORDER BY avg(score) DESC;
ConceptDescriptionBenefit
HypertableVirtual table spanning multiple chunksStandard SQL interface, automatic partitioning
ChunkPhysical partition covering a time intervalEfficient pruning, parallel query, independent compression
Chunk intervalTime range per chunk (e.g., 1 day, 1 hour)Tunable based on data volume and query patterns
Space partitioningOptional secondary partitioning (e.g., by domain)Multi-tenant workloads, parallel ingest

Chunk Interval Tuning

Choosing the right chunk interval is critical for performance. The interval determines the granularity of partition pruning, compression units, and retention operations:

Data VolumeRecommended IntervalReasoning
< 1M rows/day1 weekFewer chunks, simpler management
1M-10M rows/day1 dayGood balance of pruning and overhead
10M-100M rows/day1 hourFine-grained pruning for range queries
> 100M rows/dayCustom (minutes)Minimize per-chunk index size

The guiding principle is that each chunk should contain roughly 1-10 million rows. Too few rows per chunk wastes overhead; too many reduces the benefit of partition pruning.

Compression

TimescaleDB's native compression converts row-oriented chunks into columnar format, achieving dramatic space savings:

-- Enable compression on the hypertable
ALTER TABLE security_ratings SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'domain',
    timescaledb.compress_orderby = 'time DESC'
);

-- Add a compression policy: compress chunks older than 7 days
SELECT add_compression_policy('security_ratings', INTERVAL '7 days');
AspectUncompressedCompressed
Storage100 GB5-10 GB (90-95% reduction)
Write SpeedFull speedRead-only (must decompress to modify)
Read SpeedStandard PostgreSQLFaster for analytical queries (columnar)
Index SupportAll PostgreSQL indexesSegment-based scanning

The compress_segmentby parameter determines how compressed data is organized. Segmenting by domain means that queries filtering on a specific domain can skip segments belonging to other domains, dramatically reducing I/O for filtered queries on compressed data.

Continuous Aggregates

Continuous aggregates are materialized views that automatically and incrementally refresh as new data arrives:

-- Create a continuous aggregate for hourly security score averages
CREATE MATERIALIZED VIEW security_ratings_hourly
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', time) AS bucket,
    domain,
    avg(score) AS avg_score,
    min(score) AS min_score,
    max(score) AS max_score,
    count(*) AS measurement_count
FROM security_ratings
GROUP BY bucket, domain;

-- Add a refresh policy: update every hour, covering the last 2 hours
SELECT add_continuous_aggregate_policy('security_ratings_hourly',
    start_offset => INTERVAL '2 hours',
    end_offset => INTERVAL '1 hour',
    schedule_interval => INTERVAL '1 hour'
);

-- Query the aggregate (fast -- reads pre-computed data)
SELECT domain, avg_score
FROM security_ratings_hourly
WHERE bucket > NOW() - INTERVAL '24 hours'
ORDER BY avg_score DESC;

Continuous aggregates are incremental -- they only recompute buckets that have received new data since the last refresh. This makes them suitable for high-volume time-series workloads where recomputing the entire aggregate from raw data would be prohibitively expensive.

Hierarchical Continuous Aggregates

TimescaleDB supports layering continuous aggregates on top of each other, creating a hierarchy of pre-computed summaries:

-- Hourly aggregate (base)
CREATE MATERIALIZED VIEW ratings_hourly
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 hour', time) AS bucket, domain,
       avg(score) AS avg_score, count(*) AS count
FROM security_ratings
GROUP BY bucket, domain;

-- Daily aggregate (built on hourly)
CREATE MATERIALIZED VIEW ratings_daily
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 day', bucket) AS bucket, domain,
       avg(avg_score) AS avg_score, sum(count) AS count
FROM ratings_hourly
GROUP BY time_bucket('1 day', bucket), domain;

-- Monthly aggregate (built on daily)
CREATE MATERIALIZED VIEW ratings_monthly
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 month', bucket) AS bucket, domain,
       avg(avg_score) AS avg_score, sum(count) AS count
FROM ratings_daily
GROUP BY time_bucket('1 month', bucket), domain;

This hierarchy enables dashboards to serve different time ranges from appropriate aggregation levels -- last-hour data from the hypertable, last-day from hourly aggregates, last-month from daily, and last-year from monthly -- with consistent sub-second query performance regardless of the time range.

Retention Policies

Automated data lifecycle management through retention policies:

-- Automatically drop chunks older than 1 year
SELECT add_retention_policy('security_ratings', INTERVAL '1 year');

-- Keep raw data for 30 days, aggregated data for 2 years
SELECT add_retention_policy('security_ratings', INTERVAL '30 days');
SELECT add_retention_policy('security_ratings_hourly', INTERVAL '2 years');

TimescaleDB Functions

FunctionPurposeExample
time_bucket()Group timestamps into fixed intervalstime_bucket('5 minutes', time)
first() / last()First/last value in a time bucketfirst(score, time)
time_bucket_gapfill()Fill missing time intervals with valuesInterpolation for dashboards
histogram()Create histogram of valuesDistribution analysis
approximate_row_count()Fast approximate countDashboard counters
chunks_detailed_size()Per-chunk storage statisticsCapacity planning

Implementation in Prismatic Platform

The Prismatic Platform uses TimescaleDB for all time-series data workloads, integrated through Ecto and the platform's storage adapter layer:

  • Security Rating History: The Perimeter module stores historical security ratings (A-F grades with numeric 300-900 scores) as time-series data. Each domain's rating is tracked over time, enabling trend analysis ("has example.com's score improved this quarter?") and regression detection ("alert when a score drops more than 50 points").
  • Compliance Assessment Snapshots: NIS2 and ZKB compliance assessment results are stored as time-series records, providing an audit trail for regulatory reporting and enabling compliance trend visualization.
  • Quality Metric Trends: Platform quality metrics (13 quality domains, QDP scores, test coverage) are stored in TimescaleDB for long-term trend analysis. Continuous aggregates power the quality dashboard's hourly, daily, and weekly trend views.
  • Telemetry Event Streams: High-volume telemetry events (agent execution metrics, storage adapter latencies, API response times) are stored in hypertables with automatic compression for historical data.
  • Asset Discovery Timelines: Attack surface changes over time -- new domains discovered, certificates expiring, services appearing or disappearing -- are tracked as time-series events for change detection and alerting.
  • Observability Infrastructure: TimescaleDB serves as the storage backend for the platform's metrics pillar, complementing structured logging (text-based) and distributed tracing (span-based) with efficient numeric time-series storage.

Ecto Integration

TimescaleDB works seamlessly with Ecto through the standard PostgreSQL adapter:

defmodule PrismaticStorage.SecurityRating do
  use Ecto.Schema
  import Ecto.Query

  schema "security_ratings" do
    field :time, :utc_datetime_usec
    field :domain, :string
    field :score, :float
    field :grade, :string
    field :details, :map
  end

  @doc "Get average scores per domain over the last N days"
  def average_scores(days \\ 7) do
    from(r in __MODULE__,
      where: r.time > ago(^days, "day"),
      group_by: r.domain,
      select: %{
        domain: r.domain,
        avg_score: avg(r.score),
        min_score: min(r.score),
        max_score: max(r.score),
        count: count(r.id)
      },
      order_by: [desc: avg(r.score)]
    )
  end

  @doc "Detect score regressions exceeding threshold"
  def detect_regressions(threshold \\ 50.0) do
    from(r in __MODULE__,
      join: prev in __MODULE__,
      on: r.domain == prev.domain,
      where: r.time > ago(1, "day"),
      where: prev.time > ago(2, "day") and prev.time <= ago(1, "day"),
      group_by: r.domain,
      having: avg(fragment("?", prev.score)) - avg(r.score) > ^threshold,
      select: %{
        domain: r.domain,
        current_avg: avg(r.score),
        previous_avg: avg(fragment("?", prev.score)),
        drop: avg(fragment("?", prev.score)) - avg(r.score)
      }
    )
  end
end

Ecto Migrations for Hypertables

defmodule PrismaticStorage.Repo.Migrations.CreateSecurityRatings do
  use Ecto.Migration

  def up do
    create table(:security_ratings, primary_key: false) do
      add :time, :utc_datetime_usec, null: false
      add :domain, :string, null: false
      add :score, :float, null: false
      add :grade, :string, size: 1, null: false
      add :details, :map
    end

    # Convert to TimescaleDB hypertable
    execute "SELECT create_hypertable('security_ratings', 'time', chunk_time_interval => INTERVAL '1 day')"

    # Create indexes optimized for time-series queries
    create index(:security_ratings, [:domain, :time])

    # Enable compression
    execute """
    ALTER TABLE security_ratings SET (
      timescaledb.compress,
      timescaledb.compress_segmentby = 'domain',
      timescaledb.compress_orderby = 'time DESC'
    )
    """

    # Add compression policy
    execute "SELECT add_compression_policy('security_ratings', INTERVAL '7 days')"
  end

  def down do
    drop table(:security_ratings)
  end
end

TimescaleDB vs. Alternatives

FeatureTimescaleDBInfluxDBPrometheusStandard PostgreSQL
SQL SupportFull PostgreSQL SQLInfluxQL / FluxPromQLFull SQL
JoinsYes (with any PG table)LimitedNoYes
CompressionNative (90%+)NativeTSDB formatManual (TOAST)
Continuous AggNativeContinuous queriesRecording rulesMaterialized views (manual refresh)
EcosystemPostgreSQL tools + extensionsSpecializedSpecializedFull PostgreSQL
ScaleBillions of rowsBillions of pointsMillions of time seriesDegrades at scale
Learning CurveLow (SQL)Medium (new query language)Medium (PromQL)Low (SQL)

Performance Characteristics

WorkloadStandard PostgreSQLTimescaleDBImprovement
Insert rate~50K rows/sec~200K rows/sec4x (chunk-based parallel inserts)
Time range queryFull table scan or large indexChunk pruning10-100x faster
Aggregation (recent)Query raw dataContinuous aggregate100-1000x faster
Storage (1 year)100 GB5-10 GB10-20x smaller
Old data deletionRow-by-row DELETEDROP CHUNKNear-instant

Best Practices

Choose Chunk Intervals Based on Data Volume: The chunk interval should produce chunks of 1-10 million rows each. Too-small chunks create management overhead; too-large chunks reduce partition pruning effectiveness. Monitor chunk sizes with chunks_detailed_size() and adjust as data volume changes.

Use Continuous Aggregates for Dashboard Queries: Never query raw hypertable data for dashboard visualizations that aggregate over time. Create continuous aggregates at the granularity each dashboard panel needs, and query the aggregate instead. This reduces query latency from seconds to milliseconds.

Compress Historical Data Aggressively: Enable compression for all data older than the write window (the period where data may still be modified). For most workloads, a 7-day compression lag provides sufficient write flexibility while capturing 90%+ of storage savings.

Implement Tiered Retention: Keep raw data for short-term debugging (7-30 days), hourly aggregates for medium-term analysis (90 days to 1 year), and daily/monthly aggregates for long-term trending (2-5 years). Use retention policies to automate data lifecycle management.

  • PostgreSQL - Foundation database that TimescaleDB extends
  • Metrics - Numeric measurements stored in TimescaleDB time series
  • Observability - TimescaleDB as metrics storage backend
  • EASM - Security data with time-series storage requirements
  • Ecto - Elixir database library for TimescaleDB integration
  • Structured Logging - Complementary text-based event storage
  • Distributed Tracing - Complementary span-based request tracking
  • QDP - Quality metrics tracked as time series for trend analysis
  • Autoheal - Healing operations informed by metric trends
  • Broadway - Data pipeline producing time-series events for storage

See Also


Connect & Contribute

Created by Tomáš Korcak (korczis) | Open Source under GHL