Set up TimescaleDB continuous aggregates to automatically compute and maintain real-time materialized views for time-series data analytics. Configure refresh policies, optimize aggregate queries, and implement monitoring for production workloads.
Prerequisites
- TimescaleDB installed and configured
- PostgreSQL 13+ with superuser access
- Basic knowledge of SQL and time-series concepts
What this solves
TimescaleDB continuous aggregates provide real-time materialized views that automatically update as new data arrives, enabling fast analytical queries over large time-series datasets. This tutorial shows you how to configure continuous aggregates with optimal refresh policies, query optimization, and performance monitoring for production analytics workloads.
Step-by-step configuration
Verify TimescaleDB installation
Ensure TimescaleDB is properly installed and the extension is enabled in your database. If you need to install TimescaleDB first, follow our TimescaleDB installation guide.
sudo -u postgres psql -d your_database -c "SELECT extname, extversion FROM pg_extension WHERE extname = 'timescaledb';"
sudo -u postgres psql -d your_database -c "SELECT timescaledb_version();"
Create sample hypertable and data
Create a hypertable with sample time-series data to demonstrate continuous aggregates. This represents typical IoT sensor data with device metrics over time.
sudo -u postgres psql -d your_database
CREATE TABLE sensor_data (
time TIMESTAMPTZ NOT NULL,
device_id INTEGER NOT NULL,
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
cpu_usage DOUBLE PRECISION,
memory_usage DOUBLE PRECISION
);
SELECT create_hypertable('sensor_data', 'time');
-- Insert sample data
INSERT INTO sensor_data
SELECT
generate_series('2024-01-01'::timestamptz, '2024-01-31'::timestamptz, '1 minute') as time,
(random() * 100)::integer as device_id,
20 + random() * 15 as temperature,
30 + random() * 40 as humidity,
random() * 100 as cpu_usage,
random() * 100 as memory_usage;
\q
Create basic continuous aggregate
Create your first continuous aggregate to compute hourly averages. The WITH clause specifies that this is a materialized view that will be automatically maintained.
sudo -u postgres psql -d your_database
CREATE MATERIALIZED VIEW sensor_data_hourly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS bucket,
device_id,
COUNT(*) as data_points,
AVG(temperature) as avg_temperature,
MAX(temperature) as max_temperature,
MIN(temperature) as min_temperature,
AVG(humidity) as avg_humidity,
AVG(cpu_usage) as avg_cpu_usage,
AVG(memory_usage) as avg_memory_usage
FROM sensor_data
GROUP BY bucket, device_id;
Configure refresh policy
Set up automatic refresh policies to keep the continuous aggregate up-to-date. The refresh policy determines how often and which time ranges get refreshed.
SELECT add_continuous_aggregate_policy('sensor_data_hourly',
start_offset => INTERVAL '3 hours',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '30 minutes');
Create multi-level continuous aggregates
Create daily and weekly aggregates based on the hourly aggregate for different analytical time scales. This creates a hierarchy of pre-computed views.
-- Daily aggregate
CREATE MATERIALIZED VIEW sensor_data_daily
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', bucket) AS bucket,
device_id,
SUM(data_points) as total_data_points,
AVG(avg_temperature) as avg_temperature,
MAX(max_temperature) as max_temperature,
MIN(min_temperature) as min_temperature,
AVG(avg_humidity) as avg_humidity,
AVG(avg_cpu_usage) as avg_cpu_usage,
AVG(avg_memory_usage) as avg_memory_usage
FROM sensor_data_hourly
GROUP BY bucket, device_id;
-- Weekly aggregate
CREATE MATERIALIZED VIEW sensor_data_weekly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 week', bucket) AS bucket,
device_id,
SUM(total_data_points) as total_data_points,
AVG(avg_temperature) as avg_temperature,
MAX(max_temperature) as max_temperature,
MIN(min_temperature) as min_temperature,
AVG(avg_humidity) as avg_humidity,
AVG(avg_cpu_usage) as avg_cpu_usage,
AVG(avg_memory_usage) as avg_memory_usage
FROM sensor_data_daily
GROUP BY bucket, device_id;
Configure refresh policies for all aggregates
Set appropriate refresh intervals for daily and weekly aggregates. Longer-term aggregates typically need less frequent updates.
-- Daily aggregate refresh policy
SELECT add_continuous_aggregate_policy('sensor_data_daily',
start_offset => INTERVAL '1 week',
end_offset => INTERVAL '1 day',
schedule_interval => INTERVAL '1 hour');
-- Weekly aggregate refresh policy
SELECT add_continuous_aggregate_policy('sensor_data_weekly',
start_offset => INTERVAL '4 weeks',
end_offset => INTERVAL '1 week',
schedule_interval => INTERVAL '6 hours');
\q
Create real-time continuous aggregate
Enable real-time aggregation to include the most recent data that hasn't been materialized yet. This provides completely up-to-date results.
sudo -u postgres psql -d your_database
CREATE MATERIALIZED VIEW sensor_data_realtime
WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS
SELECT
time_bucket('15 minutes', time) AS bucket,
device_id,
COUNT(*) as data_points,
AVG(temperature) as avg_temperature,
MAX(temperature) as max_temperature,
MIN(temperature) as min_temperature,
AVG(cpu_usage) as avg_cpu_usage
FROM sensor_data
GROUP BY bucket, device_id;
SELECT add_continuous_aggregate_policy('sensor_data_realtime',
start_offset => INTERVAL '1 hour',
end_offset => INTERVAL '15 minutes',
schedule_interval => INTERVAL '15 minutes');
Optimize continuous aggregate performance
Create indexes on continuous aggregates to improve query performance and configure compression for older data.
-- Create indexes on continuous aggregates
CREATE INDEX idx_sensor_hourly_bucket_device ON sensor_data_hourly (bucket, device_id);
CREATE INDEX idx_sensor_daily_bucket_device ON sensor_data_daily (bucket, device_id);
CREATE INDEX idx_sensor_weekly_bucket_device ON sensor_data_weekly (bucket, device_id);
CREATE INDEX idx_sensor_realtime_bucket_device ON sensor_data_realtime (bucket, device_id);
-- Enable compression on continuous aggregates
ALTER MATERIALIZED VIEW sensor_data_hourly SET (timescaledb.compress = true);
ALTER MATERIALIZED VIEW sensor_data_daily SET (timescaledb.compress = true);
ALTER MATERIALIZED VIEW sensor_data_weekly SET (timescaledb.compress = true);
-- Add compression policies
SELECT add_compression_policy('sensor_data_hourly', INTERVAL '7 days');
SELECT add_compression_policy('sensor_data_daily', INTERVAL '30 days');
SELECT add_compression_policy('sensor_data_weekly', INTERVAL '90 days');
Configure PostgreSQL optimization
Tune PostgreSQL settings specifically for continuous aggregate workloads. These settings improve background worker performance and memory usage.
\q
# TimescaleDB continuous aggregate optimization
timescaledb.max_background_workers = 16
max_worker_processes = 32
shared_preload_libraries = 'timescaledb'
Memory settings for aggregation
work_mem = '256MB'
maintenance_work_mem = '1GB'
effective_cache_size = '4GB'
Checkpoint and WAL settings
checkpoint_completion_target = 0.9
wal_buffers = '16MB'
max_wal_size = '2GB'
min_wal_size = '1GB'
Background writer settings
bgwriter_delay = 200ms
bgwriter_lru_maxpages = 100
bgwriter_lru_multiplier = 2.0
Restart PostgreSQL and verify settings
Restart PostgreSQL to apply the configuration changes and verify the settings are loaded correctly.
sudo systemctl restart postgresql
sudo systemctl status postgresql
Set up monitoring views
Create monitoring views to track continuous aggregate job performance and system resource usage.
sudo -u postgres psql -d your_database
-- Create monitoring view for continuous aggregate jobs
CREATE VIEW continuous_aggregate_stats AS
SELECT
format('%I.%I', schema_name, view_name) as view_name,
materialization_hypertable_name,
job_id,
last_run_started_at,
last_successful_finish,
last_run_status,
total_runs,
total_successes,
total_failures
FROM timescaledb_information.continuous_aggregates ca
LEFT JOIN timescaledb_information.jobs j ON j.hypertable_name = ca.materialization_hypertable_name;
-- Create view for job statistics
CREATE VIEW job_stats AS
SELECT
job_id,
application_name,
job_type,
schedule_interval,
max_runtime,
max_retries,
retry_period,
last_run_started_at,
last_successful_finish,
next_start,
total_runs,
total_successes,
total_failures
FROM timescaledb_information.jobs
ORDER BY job_id;
Verify your setup
Test your continuous aggregates and verify they're working correctly with sample queries.
sudo -u postgres psql -d your_database
-- Check continuous aggregate data
SELECT * FROM sensor_data_hourly ORDER BY bucket DESC LIMIT 10;
-- Verify real-time aggregate includes latest data
SELECT * FROM sensor_data_realtime ORDER BY bucket DESC LIMIT 5;
-- Check continuous aggregate job status
SELECT * FROM continuous_aggregate_stats;
-- View compression status
SELECT
schema_name,
table_name,
compression_enabled,
compressed_chunks,
uncompressed_chunks
FROM timescaledb_information.compression_settings;
Monitor continuous aggregate performance and ensure monitoring is working:
-- Check job execution stats
SELECT * FROM job_stats WHERE job_type = 'continuous_aggregate';
-- Performance comparison query
EXPLAIN (ANALYZE, BUFFERS)
SELECT device_id, AVG(avg_temperature)
FROM sensor_data_hourly
WHERE bucket >= NOW() - INTERVAL '7 days'
GROUP BY device_id;
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Continuous aggregate not updating | Background worker not running | Check SELECT * FROM timescaledb_information.jobs and restart PostgreSQL |
| High memory usage during refresh | work_mem too low for aggregation | Increase work_mem to 256MB+ and tune maintenance_work_mem |
| Slow aggregate queries | Missing indexes on time buckets | Create indexes on (bucket, device_id) columns |
| Real-time data not appearing | materialized_only=true set | Recreate view with timescaledb.materialized_only=false |
| Compression policy failing | Active chunks being compressed | Adjust compression policy interval to avoid active data |
| Job execution failures | Resource constraints or locks | Check PostgreSQL logs and increase max_worker_processes |
Next steps
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Configuration
DATABASE_NAME="${1:-timescaledb_demo}"
PG_USER="${2:-postgres}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Usage
usage() {
echo "Usage: $0 [database_name] [postgres_user]"
echo " database_name: Database name (default: timescaledb_demo)"
echo " postgres_user: PostgreSQL user (default: postgres)"
exit 1
}
log() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
# Cleanup on error
cleanup() {
warn "Installation failed. Cleaning up..."
systemctl stop postgresql 2>/dev/null || true
}
trap cleanup ERR
# Check prerequisites
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
fi
if [[ "$#" -gt 2 ]]; then
usage
fi
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
PG_CONFIG_DIR="/etc/postgresql"
PG_SERVICE="postgresql"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
PG_CONFIG_DIR="/var/lib/pgsql"
PG_SERVICE="postgresql"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
PG_CONFIG_DIR="/var/lib/pgsql"
PG_SERVICE="postgresql"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
PG_CONFIG_DIR="/var/lib/pgsql"
PG_SERVICE="postgresql"
;;
*)
error "Unsupported distribution: $ID"
;;
esac
else
error "Cannot detect distribution. /etc/os-release not found."
fi
log "[1/8] Updating package repositories..."
$PKG_UPDATE
log "[2/8] Installing PostgreSQL and dependencies..."
case "$ID" in
ubuntu|debian)
$PKG_INSTALL wget ca-certificates gnupg lsb-release
wget --quiet -O - https://packagecloud.io/timescale/timescaledb/gpgkey | gpg --dearmor -o /usr/share/keyrings/timescaledb.gpg
echo "deb [signed-by=/usr/share/keyrings/timescaledb.gpg] https://packagecloud.io/timescale/timescaledb/debian/ $(lsb_release -c -s) main" | tee /etc/apt/sources.list.d/timescaledb.list
apt update
$PKG_INSTALL postgresql postgresql-contrib timescaledb-2-postgresql-14
;;
almalinux|rocky|centos|rhel|ol|fedora|amzn)
cat > /etc/yum.repos.d/timescaledb.repo << 'EOF'
[timescaledb]
name=TimescaleDB
baseurl=https://packagecloud.io/timescale/timescaledb/el/8/$basearch
gpgcheck=1
enabled=1
gpgkey=https://packagecloud.io/timescale/timescaledb/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
EOF
$PKG_INSTALL postgresql postgresql-server postgresql-contrib timescaledb-postgresql-14
if [ ! -d "/var/lib/pgsql/data" ]; then
postgresql-setup --initdb
fi
;;
esac
log "[3/8] Starting and enabling PostgreSQL..."
systemctl enable $PG_SERVICE
systemctl start $PG_SERVICE
log "[4/8] Configuring TimescaleDB..."
if command -v timescaledb-tune &> /dev/null; then
timescaledb-tune --quiet --yes
fi
# Restart PostgreSQL to apply configuration changes
systemctl restart $PG_SERVICE
log "[5/8] Creating database and enabling TimescaleDB extension..."
sudo -u $PG_USER createdb "$DATABASE_NAME" 2>/dev/null || warn "Database $DATABASE_NAME may already exist"
sudo -u $PG_USER psql -d "$DATABASE_NAME" -c "CREATE EXTENSION IF NOT EXISTS timescaledb;"
log "[6/8] Creating sample hypertable and data..."
sudo -u $PG_USER psql -d "$DATABASE_NAME" << 'EOF'
DROP TABLE IF EXISTS sensor_data CASCADE;
CREATE TABLE sensor_data (
time TIMESTAMPTZ NOT NULL,
device_id INTEGER NOT NULL,
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
cpu_usage DOUBLE PRECISION,
memory_usage DOUBLE PRECISION
);
SELECT create_hypertable('sensor_data', 'time');
INSERT INTO sensor_data
SELECT
generate_series('2024-01-01'::timestamptz, '2024-01-31'::timestamptz, '1 minute') as time,
(random() * 100)::integer as device_id,
20 + random() * 15 as temperature,
30 + random() * 40 as humidity,
random() * 100 as cpu_usage,
random() * 100 as memory_usage;
EOF
log "[7/8] Creating continuous aggregates..."
sudo -u $PG_USER psql -d "$DATABASE_NAME" << 'EOF'
DROP MATERIALIZED VIEW IF EXISTS sensor_data_hourly CASCADE;
CREATE MATERIALIZED VIEW sensor_data_hourly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS bucket,
device_id,
COUNT(*) as data_points,
AVG(temperature) as avg_temperature,
MAX(temperature) as max_temperature,
MIN(temperature) as min_temperature,
AVG(humidity) as avg_humidity,
AVG(cpu_usage) as avg_cpu_usage,
AVG(memory_usage) as avg_memory_usage
FROM sensor_data
GROUP BY bucket, device_id;
SELECT add_continuous_aggregate_policy('sensor_data_hourly',
start_offset => INTERVAL '3 hours',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '30 minutes');
DROP MATERIALIZED VIEW IF EXISTS sensor_data_daily CASCADE;
CREATE MATERIALIZED VIEW sensor_data_daily
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', bucket) AS bucket,
device_id,
SUM(data_points) as total_data_points,
AVG(avg_temperature) as avg_temperature,
MAX(max_temperature) as max_temperature,
MIN(min_temperature) as min_temperature,
AVG(avg_humidity) as avg_humidity,
AVG(avg_cpu_usage) as avg_cpu_usage,
AVG(avg_memory_usage) as avg_memory_usage
FROM sensor_data_hourly
GROUP BY bucket, device_id;
SELECT add_continuous_aggregate_policy('sensor_data_daily',
start_offset => INTERVAL '3 days',
end_offset => INTERVAL '1 day',
schedule_interval => INTERVAL '1 hour');
CREATE INDEX IF NOT EXISTS idx_sensor_hourly_device_time ON sensor_data_hourly (device_id, bucket);
CREATE INDEX IF NOT EXISTS idx_sensor_daily_device_time ON sensor_data_daily (device_id, bucket);
EOF
log "[8/8] Verifying installation..."
TIMESCALE_VERSION=$(sudo -u $PG_USER psql -d "$DATABASE_NAME" -t -c "SELECT timescaledb_version();" | xargs)
HYPERTABLES=$(sudo -u $PG_USER psql -d "$DATABASE_NAME" -t -c "SELECT COUNT(*) FROM timescaledb_information.hypertables;" | xargs)
CAGGS=$(sudo -u $PG_USER psql -d "$DATABASE_NAME" -t -c "SELECT COUNT(*) FROM timescaledb_information.continuous_aggregates;" | xargs)
log "Installation completed successfully!"
log "TimescaleDB version: $TIMESCALE_VERSION"
log "Hypertables created: $HYPERTABLES"
log "Continuous aggregates created: $CAGGS"
log "Database: $DATABASE_NAME"
log ""
log "Test queries:"
log "sudo -u $PG_USER psql -d $DATABASE_NAME -c \"SELECT * FROM sensor_data_hourly LIMIT 5;\""
log "sudo -u $PG_USER psql -d $DATABASE_NAME -c \"SELECT * FROM sensor_data_daily LIMIT 5;\""
Review the script before running. Execute with: bash install.sh