TimescaleDB Installation for Time-Series Data
TimescaleDB is a time-series SQL database built as a PostgreSQL extension that provides optimized storage, indexing, and query performance for time-series workloads. It automatically manages data compression, tiered storage, and efficient aggregation operations while maintaining SQL compatibility and ACID transactions. This comprehensive guide covers installation, hypertable setup, continuous aggregates, compression strategies, retention policies, and performance optimization for time-series applications.
Table of Contents
- Architecture and Concepts
- Installation
- Database and Extension Setup
- Hypertables Creation
- Indexes and Constraints
- Continuous Aggregates
- Data Compression
- Retention Policies
- Time-Series Queries
- Performance Optimization
- Conclusion
Architecture and Concepts
TimescaleDB revolutionizes time-series data management by introducing hypertables, which transparently partition time-series data into chunks. Unlike traditional tables, hypertables automatically manage data organization across time, improving both query performance and data insertion rates. Data is partitioned into time-based chunks, typically one day or one week intervals, allowing efficient compression and deletion of older data.
Continuous aggregates enable automatic calculation of pre-aggregated views without manual refresh, providing fast query results for time-bucketed metrics while background jobs keep aggregates up-to-date. Compression reduces storage requirements for historical data by up to 95%, enabling cost-effective long-term retention. TimescaleDB maintains PostgreSQL compatibility while providing custom functions optimized for time-series operations like gaps, time_bucket, and first/last aggregates.
Installation
Install PostgreSQL 13 or later as the foundation for TimescaleDB. Use the official PostgreSQL repository:
# Ubuntu 20.04/22.04
sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt-get update
sudo apt-get install -y postgresql-15 postgresql-contrib-15
# RHEL/CentOS 8/9
sudo dnf install -y gcc readline-devel zlib-devel openssl-devel
sudo dnf module enable postgresql:15
sudo dnf install -y postgresql-server postgresql-contrib
Install TimescaleDB from the official repository. Add the TimescaleDB repository:
# Ubuntu/Debian
echo "deb https://packagecloud.io/timescale/timescaledb/ubuntu/ $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/timescaledb.list
wget --quiet -O - https://packagecloud.io/timescale/timescaledb/gpgkey | sudo apt-key add -
sudo apt-get update
sudo apt-get install -y timescaledb-2-postgresql-15
# RHEL/CentOS
sudo dnf copr enable timescale/timescaledb
sudo dnf install -y timescaledb-2-postgresql-15
Initialize the PostgreSQL cluster:
# Ubuntu/Debian (PostgreSQL automatically initializes)
sudo systemctl start postgresql
sudo systemctl enable postgresql
# RHEL/CentOS
sudo postgresql-setup initdb
sudo systemctl start postgresql
sudo systemctl enable postgresql
Optimize PostgreSQL configuration for TimescaleDB. Run the tuning script:
# TimescaleDB provides an automatic tuning script
sudo timescaledb-tune --yes
# This script modifies postgresql.conf with recommended settings for your hardware
Alternatively, manually tune critical parameters:
sudo nano /etc/postgresql/15/main/postgresql.conf
Add or modify these settings:
# TimescaleDB recommended settings
shared_buffers = '256MB'
effective_cache_size = '2GB'
maintenance_work_mem = '64MB'
checkpoint_completion_target = 0.9
wal_buffers = '16MB'
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = '4MB'
min_wal_size = '1GB'
max_wal_size = '4GB'
# Enable parallel query execution
max_worker_processes = 4
max_parallel_workers_per_gather = 2
max_parallel_workers = 4
# Logging
log_min_duration_statement = 1000
log_connections = on
log_disconnections = on
log_duration = off
log_lock_waits = on
log_statement = 'all'
# Shared libraries
shared_preload_libraries = 'pg_stat_statements,timescaledb'
Restart PostgreSQL to apply settings:
sudo systemctl restart postgresql
Database and Extension Setup
Create a dedicated database for time-series data and enable the TimescaleDB extension:
sudo -u postgres psql << 'EOF'
-- Create database
CREATE DATABASE timeseries_db;
-- Connect to the new database
\c timeseries_db
-- Install TimescaleDB extension
CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
-- Verify extension installation
SELECT extname, extversion FROM pg_extension WHERE extname = 'timescaledb';
-- Check TimescaleDB version
SELECT default_version, installed_version FROM pg_available_extensions WHERE name = 'timescaledb';
EOF
Verify the extension is properly loaded:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Show installed extensions
\dx
-- Check TimescaleDB functions available
\df | grep timescaledb
-- Verify license (if using TimescaleDB Cloud)
SELECT * FROM timescaledb_information.license;
EOF
Create a role for application connections:
sudo -u postgres psql << 'EOF'
CREATE ROLE timeseries_user WITH LOGIN PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE timeseries_db TO timeseries_user;
GRANT USAGE ON SCHEMA public TO timeseries_user;
GRANT CREATE ON SCHEMA public TO timeseries_user;
EOF
Hypertables Creation
Hypertables are the core concept of TimescaleDB. They automatically partition data by time and manage chunks transparently. Create a hypertable for system metrics:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Create a standard PostgreSQL table first
CREATE TABLE system_metrics (
time TIMESTAMP WITH TIME ZONE NOT NULL,
host_id BIGINT NOT NULL,
cpu_usage REAL NOT NULL,
memory_usage REAL NOT NULL,
disk_io_read BIGINT NOT NULL,
disk_io_write BIGINT NOT NULL,
network_bytes_in BIGINT NOT NULL,
network_bytes_out BIGINT NOT NULL
);
-- Convert to hypertable with automatic chunking
SELECT create_hypertable('system_metrics', 'time', if_not_exists => TRUE);
-- Verify hypertable creation
SELECT hypertable_name, owner, created_at FROM timescaledb_information.hypertables;
-- Check chunk information
SELECT chunk_name, range_start, range_end, is_compressed FROM timescaledb_information.chunks
WHERE hypertable_name = 'system_metrics';
EOF
Create additional hypertables for different metric types:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Application metrics hypertable
CREATE TABLE app_metrics (
time TIMESTAMP WITH TIME ZONE NOT NULL,
app_id BIGINT NOT NULL,
request_count BIGINT NOT NULL,
response_time_ms REAL NOT NULL,
error_count BIGINT NOT NULL,
status_code INT,
endpoint VARCHAR(255)
);
SELECT create_hypertable('app_metrics', 'time', if_not_exists => TRUE);
-- Database performance metrics
CREATE TABLE db_metrics (
time TIMESTAMP WITH TIME ZONE NOT NULL,
db_id BIGINT NOT NULL,
connections_active INT NOT NULL,
queries_total BIGINT NOT NULL,
transactions_committed BIGINT NOT NULL,
transactions_rollback BIGINT NOT NULL,
cache_hit_ratio REAL NOT NULL
);
SELECT create_hypertable('db_metrics', 'time', if_not_exists => TRUE);
EOF
Indexes and Constraints
Create indexes to optimize query performance on hypertables. Add appropriate indexes:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Time index (automatically created by hypertable)
-- Already exists, but verify
SELECT indexname FROM pg_indexes WHERE tablename = 'system_metrics';
-- Create additional indexes on frequently filtered columns
CREATE INDEX ON system_metrics (host_id DESC, time DESC);
CREATE INDEX ON app_metrics (app_id DESC, time DESC);
CREATE INDEX ON db_metrics (db_id DESC, time DESC);
-- Optional: Create multi-column index for common queries
CREATE INDEX ON system_metrics (time DESC, host_id, cpu_usage);
-- Verify index creation
SELECT indexname, indexdef FROM pg_indexes WHERE tablename IN ('system_metrics', 'app_metrics', 'db_metrics')
ORDER BY tablename;
EOF
Add constraints to ensure data integrity:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Add constraints to hypertables
ALTER TABLE system_metrics
ADD CONSTRAINT cpu_usage_valid CHECK (cpu_usage >= 0 AND cpu_usage <= 100),
ADD CONSTRAINT memory_usage_valid CHECK (memory_usage >= 0 AND memory_usage <= 100);
ALTER TABLE app_metrics
ADD CONSTRAINT request_count_positive CHECK (request_count >= 0),
ADD CONSTRAINT response_time_positive CHECK (response_time_ms >= 0);
ALTER TABLE db_metrics
ADD CONSTRAINT connections_positive CHECK (connections_active >= 0),
ADD CONSTRAINT cache_hit_valid CHECK (cache_hit_ratio >= 0 AND cache_hit_ratio <= 1);
-- List constraints
\d system_metrics
EOF
Continuous Aggregates
Continuous aggregates provide pre-computed, automatically-updated materialized views for efficient downsampling and aggregation:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Create continuous aggregate for hourly system metrics
CREATE MATERIALIZED VIEW system_metrics_hourly WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS hour,
host_id,
AVG(cpu_usage) AS avg_cpu,
MAX(cpu_usage) AS max_cpu,
MIN(cpu_usage) AS min_cpu,
AVG(memory_usage) AS avg_memory,
MAX(memory_usage) AS max_memory,
SUM(disk_io_read) AS total_io_read,
SUM(disk_io_write) AS total_io_write
FROM system_metrics
GROUP BY hour, host_id
WITH DATA;
-- Create index on continuous aggregate
CREATE INDEX ON system_metrics_hourly (host_id DESC, hour DESC);
-- Create daily aggregate
CREATE MATERIALIZED VIEW system_metrics_daily WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', time) AS day,
host_id,
AVG(cpu_usage) AS avg_cpu,
MAX(cpu_usage) AS max_cpu,
AVG(memory_usage) AS avg_memory,
MAX(memory_usage) AS max_memory
FROM system_metrics
GROUP BY day, host_id
WITH DATA;
-- Create continuous aggregate for application metrics
CREATE MATERIALIZED VIEW app_metrics_1min WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 minute', time) AS minute,
app_id,
SUM(request_count) AS requests,
AVG(response_time_ms) AS avg_response_time,
MAX(response_time_ms) AS max_response_time,
SUM(error_count) AS errors
FROM app_metrics
GROUP BY minute, app_id
WITH DATA;
-- Verify continuous aggregates
SELECT * FROM timescaledb_information.continuous_aggregates;
EOF
Configure refresh policies for continuous aggregates:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Add refresh policy for hourly aggregate (refresh every 30 minutes)
SELECT add_continuous_aggregate_policy('system_metrics_hourly',
start_offset => INTERVAL '2 hours',
end_offset => INTERVAL '30 minutes',
schedule_interval => INTERVAL '30 minutes');
-- Add refresh policy for daily aggregate
SELECT add_continuous_aggregate_policy('system_metrics_daily',
start_offset => INTERVAL '2 days',
end_offset => INTERVAL '1 day',
schedule_interval => INTERVAL '1 day');
-- Add refresh policy for app metrics
SELECT add_continuous_aggregate_policy('app_metrics_1min',
start_offset => INTERVAL '2 hours',
end_offset => INTERVAL '5 minutes',
schedule_interval => INTERVAL '5 minutes');
-- Check policy status
SELECT * FROM timescaledb_information.continuous_aggregate_policy;
EOF
Data Compression
Data compression significantly reduces storage requirements for historical data. Enable and configure compression:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Enable compression for system_metrics with 7-day retention
SELECT add_compression_policy('system_metrics', INTERVAL '7 days');
-- Enable compression for app_metrics (3-day retention)
SELECT add_compression_policy('app_metrics', INTERVAL '3 days');
-- Enable compression for db_metrics (14-day retention)
SELECT add_compression_policy('db_metrics', INTERVAL '14 days');
-- Check compression policy status
SELECT * FROM timescaledb_information.compression_settings;
EOF
Configure compression parameters for optimal performance:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Get compression settings information
SELECT hypertable_name, compression_enabled, compress_interval_length
FROM timescaledb_information.compression_settings;
-- Manually compress older chunks (if needed)
SELECT compress_chunk(i)
FROM show_chunks('system_metrics', INTERVAL '30 days ago') i;
-- Check compression status
SELECT chunk_name, is_compressed, uncompressed_bytes_before, uncompressed_bytes_after
FROM timescaledb_information.chunks
WHERE hypertable_name = 'system_metrics'
ORDER BY range_start DESC;
EOF
Retention Policies
Implement automatic data retention policies to manage storage and costs:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Add retention policy to system_metrics (keep 90 days)
SELECT add_retention_policy('system_metrics', INTERVAL '90 days', if_not_exists => true);
-- Add retention policy to app_metrics (keep 30 days)
SELECT add_retention_policy('app_metrics', INTERVAL '30 days', if_not_exists => true);
-- Add retention policy to db_metrics (keep 60 days)
SELECT add_retention_policy('db_metrics', INTERVAL '60 days', if_not_exists => true);
-- View retention policies
SELECT * FROM timescaledb_information.retention_policies;
EOF
Monitor retention operations:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Check when chunks will be dropped
SELECT chunk_name, range_start, range_end
FROM timescaledb_information.chunks
WHERE hypertable_name = 'system_metrics'
AND range_end < NOW() - INTERVAL '90 days'
ORDER BY range_start DESC;
-- View job execution history
SELECT job_id, job_type, last_successful_finish
FROM timescaledb_information.jobs
WHERE job_type IN ('retention', 'compression', 'continuous_aggregate');
EOF
Time-Series Queries
Utilize TimescaleDB-specific functions for efficient time-series analysis:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Time bucketing query
SELECT
time_bucket('5 minutes', time) AS period,
host_id,
AVG(cpu_usage) AS avg_cpu,
MAX(memory_usage) AS peak_memory
FROM system_metrics
WHERE time > NOW() - INTERVAL '1 day'
GROUP BY period, host_id
ORDER BY period DESC, host_id;
-- First/last aggregates (very efficient)
SELECT
host_id,
FIRST(cpu_usage, time) AS first_cpu,
LAST(cpu_usage, time) AS last_cpu,
FIRST(memory_usage, time) AS first_memory,
LAST(memory_usage, time) AS last_memory
FROM system_metrics
WHERE time > NOW() - INTERVAL '24 hours'
GROUP BY host_id;
-- Rate function for calculating per-second rates
SELECT
time_bucket('1 minute', time) AS minute,
host_id,
RATE(SUM(disk_io_read)) AS io_read_bytes_per_sec,
RATE(SUM(disk_io_write)) AS io_write_bytes_per_sec
FROM system_metrics
WHERE time > NOW() - INTERVAL '6 hours'
GROUP BY minute, host_id
ORDER BY minute DESC;
-- Gap filling for missing data
SELECT
time_bucket_gapfill('5 minutes', time) AS period,
host_id,
LOCF(AVG(cpu_usage)) AS cpu_usage
FROM system_metrics
WHERE time > NOW() - INTERVAL '1 day'
AND host_id = 1
GROUP BY period, host_id
ORDER BY period DESC;
EOF
Complex analytical queries:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Anomaly detection: find periods with CPU > 2 standard deviations from mean
SELECT
time,
host_id,
cpu_usage,
AVG(cpu_usage) OVER (PARTITION BY host_id ORDER BY time ROWS BETWEEN 100 PRECEDING AND CURRENT ROW) AS rolling_avg,
STDDEV(cpu_usage) OVER (PARTITION BY host_id ORDER BY time ROWS BETWEEN 100 PRECEDING AND CURRENT ROW) AS rolling_stddev
FROM system_metrics
WHERE time > NOW() - INTERVAL '24 hours'
AND cpu_usage > (
SELECT AVG(cpu_usage) + (2 * STDDEV(cpu_usage))
FROM system_metrics
WHERE time > NOW() - INTERVAL '7 days'
AND host_id = system_metrics.host_id
)
ORDER BY time DESC;
-- Compare periods (today vs yesterday)
SELECT
DATE_TRUNC('hour', time) AS hour,
SUM(CASE WHEN DATE(time) = DATE(NOW()) THEN request_count ELSE 0 END) AS today_requests,
SUM(CASE WHEN DATE(time) = DATE(NOW() - INTERVAL '1 day') THEN request_count ELSE 0 END) AS yesterday_requests
FROM app_metrics
WHERE time > NOW() - INTERVAL '48 hours'
GROUP BY hour
ORDER BY hour DESC;
EOF
Performance Optimization
Optimize TimescaleDB performance for your workload:
# Run analyze to update table statistics
sudo -u postgres psql -d timeseries_db << 'EOF'
ANALYZE system_metrics;
ANALYZE app_metrics;
ANALYZE db_metrics;
-- Check table sizes
SELECT
hypertable_name,
pg_size_pretty(total_bytes) AS total_size,
pg_size_pretty(index_bytes) AS index_size
FROM timescaledb_information.hypertable_size
ORDER BY total_bytes DESC;
-- Check chunk count (too many small chunks can degrade performance)
SELECT hypertable_name, COUNT(*) as chunk_count
FROM timescaledb_information.chunks
GROUP BY hypertable_name;
EOF
Monitor slow queries:
# Enable query logging in postgresql.conf
sudo nano /etc/postgresql/15/main/postgresql.conf
# Add
log_min_duration_statement = 1000 # Log queries taking more than 1 second
log_duration = off
log_statement = 'all'
sudo systemctl restart postgresql
# Query slow logs
sudo tail -f /var/log/postgresql/postgresql-15-main.log | grep "duration:"
Optimize chunk interval for your workload:
sudo -u postgres psql -d timeseries_db << 'EOF'
-- Change chunk interval (affects new chunks)
SELECT set_chunk_time_interval('system_metrics', INTERVAL '1 week');
-- View current chunk size
SELECT
hypertable_name,
dimension_name,
chunk_time_interval
FROM timescaledb_information.dimensions;
EOF
Conclusion
TimescaleDB transforms PostgreSQL into a specialized time-series database while maintaining full SQL compatibility and ACID properties. By leveraging hypertables for automatic data organization, continuous aggregates for efficient pre-computation, and compression for storage optimization, you can manage massive volumes of time-series data cost-effectively. The combination of automatic partitioning, compression policies, and retention management enables scalable monitoring and analytics infrastructure. Whether storing infrastructure metrics, application performance data, or financial tick data, TimescaleDB provides the tools to query and analyze historical trends efficiently while maintaining fast ingestion rates and storage efficiency.


