Quick Start

Updated at:

GanosBase TSDB is a time-series database plug-in for PolarDB for PostgreSQL. It inherits all capabilities of a PolarDB for PostgreSQL cluster, including shared storage, one-write-multiple-reads, and backup and recovery. It is fully compatible with TimescaleDB (Apache 2.0) and adds advanced features — continuous aggregation, data compression, and OSS tiered storage for hot and cold data — that are not available in the open-source edition.

Prerequisites

Before you begin, ensure that you have:

  • A PolarDB for PostgreSQL cluster running PostgreSQL 14 (minor engine version 2.0.14.13.26.0 or later) or PostgreSQL 16 (minor engine version 2.0.16.9.8.0 or later)

To check your minor engine version, run SHOW polardb_version; or view it in the console. If the version does not meet the requirement, upgrade the minor engine version.

Key concepts

Two concepts are central to using GanosBase TSDB effectively:

  • Hypertable: A special table type that automatically partitions time-series data by time (and optionally by other dimensions). All standard SQL operations work on hypertables, with added time-series capabilities. The default partition interval is 7 days.

  • Continuous aggregate: An automated precomputation mechanism backed by a materialized view. It incrementally updates predefined aggregations — such as hourly averages — so queries read precomputed results instead of scanning raw data.

Enable the time-series database

  1. Add timescaledb to the shared_preload_libraries parameter for your cluster. Follow the steps in modify the shared_preload_libraries parameter.

    Important

    Modifying shared_preload_libraries restarts the cluster. Plan your maintenance window before making this change.

Create the Plug-in

Create the GanosBase TSDB extension. The extension depends on TimescaleDB, so create both using one of these methods: Method 1 (recommended): Use CASCADE to create both extensions at once.

To avoid permission issues, install extensions in the PUBLIC schema: ``sql CREATE EXTENSION ganos_tsdb WITH SCHEMA PUBLIC CASCADE; ` If you see ERROR: Disable the injection of custom functions when creating extension: metadata_insert_trigger (21128)`, contact support to enable the required permissions before installing.
CREATE EXTENSION ganos_tsdb CASCADE;

Method 2: Create extensions individually.

CREATE EXTENSION timescaledb;
CREATE EXTENSION ganos_tsdb;

Upgrade the plug-in

To upgrade GanosBase TSDB or TimescaleDB in an existing cluster, upgrade TimescaleDB first, then GanosBase TSDB:

-- Upgrade TimescaleDB first
ALTER EXTENSION timescaledb UPDATE;

-- Then upgrade GanosBase TSDB
ALTER EXTENSION ganos_tsdb UPDATE;

Work with hypertables

A hypertable partitions time-series data into chunks by time. Each chunk covers a specific time range and stores only data from that range. When you insert data for a time range that has no chunk yet, the system creates one automatically.

Hypertables coexist with regular tables and support all standard SQL operations. The default partition interval is 7 days.

Create a hypertable

Convert a standard table to a hypertable using create_hypertable:

SELECT create_hypertable(
    'table_name',
    'time_column_name',
    'partitioning_column_name',
    number_partitions,
    'associated_schema_name',
    'associated_table_prefix',
    chunk_time_interval,
    create_default_indexes,
    if_not_exists,
    partitioning_func,
    migrate_data,
    chunk_target_size,
    chunk_sizing_func,
    time_partitioning_func
);
ParameterRequiredDescription
table_nameYesName or OID of the source table. The hypertable keeps the same name after conversion.
time_column_nameYesName of the time column. Supported types: TIMESTAMP, TIMESTAMPTZ, DATE, integer (interpreted as microseconds), or INTERVAL.
partitioning_column_nameNoName of the partitioning column. Defaults to the time column.
number_partitionsNoNumber of partitions.
associated_schema_nameNoSchema where the hypertable resides.
associated_table_prefixNoPrefix for chunk tables.
chunk_time_intervalNoTime interval for chunks. Default is 7 days.
create_default_indexesNoWhether to create a B-tree index on the time column. true (default): creates the index. false: skips it.
if_not_existsNoWhether to suppress an error if the hypertable already exists. true: warns but does not raise an error. false (default): raises an error if the hypertable already exists.
partitioning_funcNoCustom spatial partitioning function, if using spatial partitioning.
migrate_dataNoWhether to move existing data from the source table. true: transfers existing data. false (default): keeps data in the source table.
chunk_target_sizeNoTarget size for chunks (for example, '1000MB', 'estimate', or 'off').
chunk_sizing_funcNoCustom function to calculate the chunk time interval. Used with chunk_target_size.
time_partitioning_funcNoCustom partition function for time partitioning.

Change the partition interval

To change the chunk time interval after creating a hypertable:

SELECT set_chunk_time_interval(
    'table_name',
    chunk_time_interval,
    'dimension_name'
);
ParameterDescription
table_nameName of the hypertable.
chunk_time_intervalNew time interval for chunks.
dimension_name(Optional) The partition dimension to update. Default is NULL.

Example: create a hypertable for transaction data

This example creates a hypertable for financial transaction data, partitioned by day.

  1. Create a standard table:

    CREATE TABLE transaction_data(
       tm TIMESTAMPTZ NOT NULL,
       id INT NOT NULL,
       price double precision);
  2. Set the replication identity:

    ALTER TABLE transaction_data REPLICA IDENTITY DEFAULT;
  3. Convert to a hypertable with a 1-day chunk interval:

    If the table already contains data, set migrate_data => true. Migration may take a long time for large tables.
    SELECT create_hypertable('transaction_data', 'tm', chunk_time_interval => INTERVAL '1 day');
  4. (Optional) Change the chunk interval to 2 days:

    SELECT set_chunk_time_interval('transaction_data', chunk_time_interval => INTERVAL '2 day');

Work with continuous aggregates

Choose an aggregation approach

Before creating a continuous aggregate, choose the right aggregation type for your use case:

TypeHow it worksBest for
Materialized viewStandard PostgreSQL. Refreshed manually with REFRESH MATERIALIZED VIEW.One-time or infrequent aggregations where freshness is not critical.
Continuous aggregateGanosBase TSDB feature. Refreshed incrementally in the background. Only new or changed data is recomputed.Regular aggregations on large time-series datasets where query speed matters.
Real-time aggregateA continuous aggregate with real-time query enabled. Combines precomputed results with the latest raw data not yet aggregated.Queries that need up-to-the-second results on top of historical aggregations.

Use a continuous aggregate when you need fast, recurring queries on large datasets. Enable real-time query on top if you also need the most recent, unaggregated data.

Create a continuous aggregate

A continuous aggregate is declared as a materialized view with timescaledb.continuous:

CREATE MATERIALIZED VIEW transaction_min_cagg
    WITH (timescaledb.continuous) -- Declare as a continuous aggregate
    AS
    SELECT id,
        time_bucket(INTERVAL '1 min', tm) AS bucket, -- Aggregate by 1-minute intervals
        AVG(price),
        MAX(price),
        MIN(price)
    FROM transaction_data
    GROUP BY id, bucket;

Results are stored in time-aligned chunks that match the underlying hypertable's partitioning. Continuous aggregates support incremental updates — only buckets with new or modified data are recomputed.

Enable real-time queries

By default, querying a continuous aggregate returns only completed aggregations. Enable real-time query to include newly ingested data that has not yet been aggregated:

-- Enable real-time query
ALTER MATERIALIZED VIEW transaction_min_cagg SET (timescaledb.materialized_only = false);

-- Disable real-time query
ALTER MATERIALIZED VIEW transaction_min_cagg SET (timescaledb.materialized_only = true);

Create nested aggregates

Build continuous aggregates on top of other continuous aggregates to reduce redundancy and improve performance. For example, create hourly aggregates from per-minute ones:

  1. Create a per-minute aggregate:

    CREATE MATERIALIZED VIEW transaction_min_cagg
        WITH (timescaledb.continuous) AS
        SELECT id,
            time_bucket(INTERVAL '1 min', tm) AS bucket,
            AVG(price) AS avg,
            MAX(price) AS max,
            MIN(price) AS min
        FROM transaction_data
        GROUP BY id, bucket;
  2. Create an hourly aggregate from the per-minute aggregate:

    CREATE MATERIALIZED VIEW transaction_one_hour_mview
        WITH (timescaledb.continuous) AS
        SELECT id,
            time_bucket(INTERVAL '1 hour', bucket) AS bucket,
            AVG(avg) AS avg,
            MAX(max) AS max,
            MIN(min) AS min
        FROM transaction_min_cagg
        GROUP BY id, time_bucket(INTERVAL '1 hour', bucket);

Refresh a continuous aggregate

Refresh manually

Call refresh_continuous_aggregate to refresh a specific time window:

refresh_continuous_aggregate(
    cagg     REGCLASS,
    window_start             "any",
    window_end               "any"
);
ParameterDescription
caggName or OID of the continuous aggregate to refresh.
window_startStart of the refresh window. Must match the time column type. If not aligned to a unit boundary, use date_trunc — for example, for minute-level aggregation, set 2024-03-11 13:01:29 to 2024-03-11 13:01:00. Avoid NULL: it triggers a full table scan, which can be slow on large tables.
window_endEnd of the refresh window. Set to NULL to refresh up to the latest data.

Usage notes:

  • The refresh window must align exactly with the time buckets. Only fully covered buckets are refreshed; partial buckets are skipped.

  • If multiple refresh calls have overlapping windows, only buckets with new or modified data are updated.

  • refresh_continuous_aggregate is a stored procedure — call it with CALL.

Example:

CALL refresh_continuous_aggregate('transaction_min_cagg', '2024-03-11 00:00:00+08'::timestamptz, NULL);

Refresh automatically

Two options are available for automatic refresh.

(Recommended) Combine withJob Scheduling

Option 1 (recommended): Use `add_job` for flexible scheduling

This approach gives you full control over the refresh window and schedule:

  1. Define the refresh logic as a stored procedure:

    CREATE OR REPLACE PROCEDURE transaction_min_cagg_refresh() LANGUAGE PLPGSQL AS
    $$
    DECLARE
    BEGIN
        -- Refresh from '2024-03-11 00:00:00+08' to the latest data
        CALL refresh_continuous_aggregate('transaction_min_cagg', '2024-03-11 00:00:00+08'::timestamptz, NULL);
    END
    $$;
  2. Schedule the job to run every minute, starting at a specific time:

    initial_start (job start time) and window_start (refresh window start) are different parameters. Set initial_start to an exact hour or leave it NULL to start immediately.
    SELECT add_job('transaction_min_cagg_refresh', '1 min', initial_start => '2024-04-01 00:00:00+08'::timestamptz);

Use add_continuous_aggregate_policy

Option 2: Use `add_continuous_aggregate_policy`

Create a refresh policy that automatically triggers periodic refreshes based on offsets from the current time:

integer add_continuous_aggregate_policy(
    cagg REGCLASS,
    start_offset "any",
    end_offset "any",
    schedule_interval INTERVAL,
    if_not_exists BOOL = false,
    initial_start TIMESTAMPTZ = NULL,
    timezone TEXT = NULL
);
ParameterDescription
caggName or OID of the continuous aggregate.
start_offsetOffset from the current time defining the start of the refresh window. Must be greater than end_offset. NULL means the window starts at the earliest data in the hypertable.
end_offsetOffset from the current time defining the end of the refresh window. NULL means the window ends at the latest data in the hypertable.
schedule_intervalRefresh interval. Default is 1 day.
if_not_existsfalse (default): returns an error if the policy already exists. true: warns but does not error.
initial_startFirst execution time. Default is NULL. Works with schedule_interval when set.
timezoneTime zone. Default is NULL (UTC).

Example:

SELECT add_continuous_aggregate_policy('transaction_min_cagg',
    start_offset => '1 hour', -- Process data from 1 hour ago
    end_offset => INTERVAL '0', -- Process up to current time
    schedule_interval => INTERVAL '1 sec' -- Refresh every second
);

Manage scheduled jobs

GanosBase TSDB uses a job scheduling framework to run functions or stored procedures on a schedule. The following functions manage jobs.

Create a job

integer add_job(
    proc REGPROC,
    schedule_interval INTERVAL,
    config JSONB DEFAULT NULL,
    initial_start TIMESTAMPTZ DEFAULT NULL,
    scheduled BOOL DEFAULT true,
    check_config REGPROC DEFAULT NULL,
    fixed_schedule BOOL DEFAULT TRUE,
    timezone TEXT DEFAULT NULL
);
ParameterDescription
procName or OID of the function or stored procedure to run.
schedule_intervalInterval between job runs.
configJob configuration parameters.
initial_startStart time for the job. Default is now.
scheduledWhether to run automatically. Default is true.
check_configFunction to validate the config parameter. Default is NULL.
fixed_scheduleWhether to run at fixed intervals. Default is true.
timezoneTime zone. Default is NULL.

Example:

  1. Create the stored procedure to run:

    CREATE OR REPLACE PROCEDURE user_defined_action(job_id int, config jsonb) LANGUAGE PLPGSQL AS
    $$
    BEGIN
      RAISE NOTICE 'Executing action % with config %', job_id, config;
    END
    $$;
  2. Create the job. The function returns the job ID:

    -- Run every hour, starting immediately
    SELECT add_job('user_defined_action', '1 hour');
    
    -- Run every hour, starting at a specific time
    SELECT add_job('user_defined_action', '1h', initial_start => '2024-03-11 00:00:00');

Modify a job

void alter_job(
    job_id INTEGER,
    schedule_interval INTERVAL = NULL,
    max_runtime INTERVAL = NULL,
    max_retries INTEGER = NULL,
    retry_period INTERVAL = NULL,
    scheduled BOOL = NULL,
    config JSONB = NULL,
    next_start TIMESTAMPTZ = NULL,
    if_exists BOOL = FALSE,
    check_config REGPROC = NULL,
    fixed_schedule BOOL = NULL,
    initial_start TIMESTAMPTZ = NULL,
    timezone TEXT DEFAULT NULL
);

Key parameters (see add_job for the full list):

ParameterDescription
job_idID of the job to modify.
max_runtimeMaximum runtime for the job. Default is NULL.
max_retriesMaximum number of retries after failure. Default is NULL.
retry_periodTime between retries after failure. Default is NULL.

Example:

SELECT alter_job(1002, schedule_interval => INTERVAL '2 hours');

Run a job manually

void run_job(job_id int);
ParameterDescription
job_idID of the job to run.

Example:

CALL run_job(1002);

Delete a job

void delete_job(job_id int);
ParameterDescription
job_idID of the job to delete.

Example:

SELECT delete_job(1002);

Enable time-series compression

When a time-series partition becomes historical data, compress it to reduce storage costs. GanosBase TSDB supports compressing entire tables or individual partitions, reducing storage space by over 70%.

Compressed data is read-only.

Uninstall the plug-in

DROP EXTENSION ganos_tsdb CASCADE;
DROP EXTENSION timescaledb CASCADE;