TimescaleDB (Time-Series Database) Patterns
1. TimescaleDB Setup
Installation
sql
-- Install TimescaleDB extension CREATE EXTENSION IF NOT EXISTS timescaledb; -- Check TimescaleDB version SELECT extversion FROM pg_extension WHERE extname = 'timescaledb';
Database Initialization
sql
-- Create database CREATE DATABASE timeseries_db; -- Connect to database \c timeseries_db; -- Enable TimescaleDB CREATE EXTENSION IF NOT EXISTS timescaledb; -- Verify installation SELECT * FROM timescaledb_information.hypertables;
2. Hypertable Creation
Basic Hypertable
sql
-- Create a basic time-series table
CREATE TABLE sensor_data (
time TIMESTAMPTZ NOT NULL,
sensor_id INTEGER NOT NULL,
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
pressure DOUBLE PRECISION
);
-- Convert to hypertable
SELECT create_hypertable('sensor_data', 'time');
-- Verify hypertable
SELECT * FROM timescaledb_information.hypertables WHERE hypertable_name = 'sensor_data';
Hypertable with Multiple Dimensions
sql
-- Create table with multiple dimensions
CREATE TABLE iot_metrics (
time TIMESTAMPTZ NOT NULL,
device_id INTEGER NOT NULL,
location_id INTEGER NOT NULL,
metric_type TEXT NOT NULL,
value DOUBLE PRECISION,
metadata JSONB
);
-- Create hypertable with time and space partitioning
SELECT create_hypertable(
'iot_metrics',
'time',
partitioning_column => 'device_id',
number_partitions => 4
);
Hypertable with Custom Chunk Size
sql
-- Create hypertable with custom chunk interval (1 day)
SELECT create_hypertable(
'sensor_data',
'time',
chunk_time_interval => interval '1 day'
);
-- Create hypertable with custom chunk interval (1 hour for high-frequency data)
SELECT create_hypertable(
'high_frequency_data',
'time',
chunk_time_interval => interval '1 hour'
);
3. Time-based Partitioning
Automatic Partitioning
sql
-- TimescaleDB automatically partitions data by time
-- Chunks are created based on the chunk_time_interval
-- View chunks for a hypertable
SELECT *
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_start;
-- View chunk sizes
SELECT
chunk_schema,
chunk_name,
range_start,
range_end,
pg_size_pretty(pg_total_relation_size(chunk_schema || '.' || chunk_name)) as size
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_start;
Manual Chunk Management
sql
-- Show chunks
SELECT show_chunks('sensor_data');
-- Show chunks for specific time range
SELECT show_chunks('sensor_data', NOW() - INTERVAL '7 days');
-- Create chunk for future data
SELECT create_chunk('sensor_data', NOW() + INTERVAL '1 day');
-- Drop old chunks
SELECT drop_chunks('sensor_data', INTERVAL '30 days');
4. Continuous Aggregates
Basic Continuous Aggregate
sql
-- Create a continuous aggregate for hourly averages
CREATE MATERIALIZED VIEW sensor_hourly_avg
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature,
AVG(humidity) AS avg_humidity,
AVG(pressure) AS avg_pressure,
COUNT(*) AS count
FROM sensor_data
GROUP BY bucket, sensor_id;
-- Refresh continuous aggregate
CALL refresh_continuous_aggregate('sensor_hourly_avg', NULL, NULL);
Continuous Aggregate with Multiple Time Buckets
sql
-- Daily aggregates
CREATE MATERIALIZED VIEW sensor_daily_avg
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature,
MIN(temperature) AS min_temperature,
MAX(temperature) AS max_temperature,
STDDEV(temperature) AS stddev_temperature,
COUNT(*) AS count
FROM sensor_data
GROUP BY bucket, sensor_id;
-- Weekly aggregates
CREATE MATERIALIZED VIEW sensor_weekly_avg
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 week', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature,
MIN(temperature) AS min_temperature,
MAX(temperature) AS max_temperature,
COUNT(*) AS count
FROM sensor_data
GROUP BY bucket, sensor_id;
Continuous Aggregate with Real-time Aggregation
sql
-- Create continuous aggregate with real-time aggregation
CREATE MATERIALIZED VIEW sensor_hourly_avg_realtime
WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature,
AVG(humidity) AS avg_humidity,
COUNT(*) AS count
FROM sensor_data
GROUP BY bucket, sensor_id;
-- Query with real-time data
SELECT * FROM sensor_hourly_avg_realtime
WHERE bucket >= NOW() - INTERVAL '1 day';
Continuous Aggregate with Custom Refresh Policy
sql
-- Set refresh policy
SELECT add_continuous_aggregate_policy(
'sensor_hourly_avg',
start_offset => INTERVAL '1 hour',
end_offset => INTERVAL '1 minute',
schedule_interval => INTERVAL '5 minutes'
);
-- View continuous aggregate policies
SELECT * FROM timescaledb_information.jobs
WHERE hypertable_name = 'sensor_hourly_avg';
-- Remove policy
SELECT remove_continuous_aggregate_policy('sensor_hourly_avg');
5. Data Retention Policies
Drop Chunks Policy
sql
-- Create drop chunks policy to retain data for 30 days
SELECT add_drop_chunks_policy(
'sensor_data',
INTERVAL '30 days',
schedule_interval => INTERVAL '1 day'
);
-- View drop chunks policies
SELECT * FROM timescaledb_information.jobs
WHERE hypertable_name = 'sensor_data';
-- Remove drop chunks policy
SELECT remove_drop_chunks_policy('sensor_data');
Compress Chunks Policy
sql
-- Enable compression on hypertable
ALTER TABLE sensor_data SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'sensor_id'
);
-- Create compress chunks policy
SELECT add_compression_policy(
'sensor_data',
INTERVAL '7 days',
schedule_interval => INTERVAL '1 day'
);
-- View compression policies
SELECT * FROM timescaledb_information.jobs
WHERE hypertable_name = 'sensor_data';
-- Remove compression policy
SELECT remove_compression_policy('sensor_data');
Combined Retention Policy
sql
-- Create combined policy: compress after 7 days, drop after 90 days
SELECT add_compression_policy(
'sensor_data',
INTERVAL '7 days',
schedule_interval => INTERVAL '1 day'
);
SELECT add_drop_chunks_policy(
'sensor_data',
INTERVAL '90 days',
schedule_interval => INTERVAL '1 day'
);
6. Query Optimization for Time-Series
Time Bucket Queries
sql
-- Basic time bucket
SELECT
time_bucket('5 minutes', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY bucket, sensor_id
ORDER BY bucket;
-- Time bucket with offset
SELECT
time_bucket('1 hour', time, TIMESTAMP '2020-01-01 00:00:00') AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY bucket, sensor_id
ORDER BY bucket;
Time Bucket Gap Filling
sql
-- Fill missing time buckets with NULL
SELECT
time_bucket('5 minutes', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 hour'
GROUP BY bucket, sensor_id
ORDER BY bucket;
-- Fill missing time buckets with previous value (LOCF)
SELECT
time_bucket('5 minutes', time) AS bucket,
sensor_id,
locf(AVG(temperature)) AS avg_temperature
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 hour'
GROUP BY bucket, sensor_id
ORDER BY bucket;
-- Fill missing time buckets with linear interpolation
SELECT
time_bucket('5 minutes', time) AS bucket,
sensor_id,
interpolate(AVG(temperature)) AS avg_temperature
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 hour'
GROUP BY bucket, sensor_id
ORDER BY bucket;
Latest Data Query
sql
-- Get latest data for each sensor
SELECT DISTINCT ON (sensor_id)
sensor_id,
time,
temperature,
humidity,
pressure
FROM sensor_data
ORDER BY sensor_id, time DESC;
-- Using LAST() function
SELECT
sensor_id,
last(temperature, time) AS latest_temperature,
last(humidity, time) AS latest_humidity,
last(pressure, time) AS latest_pressure
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY sensor_id;
First/Last Data Query
sql
-- Get first data point in time range
SELECT
sensor_id,
first(temperature, time) AS first_temperature,
first(humidity, time) AS first_humidity
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY sensor_id;
-- Get last data point in time range
SELECT
sensor_id,
last(temperature, time) AS last_temperature,
last(humidity, time) AS last_humidity
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY sensor_id;
Delta and Rate Calculations
sql
-- Calculate delta between consecutive values
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature,
delta(AVG(temperature)) OVER (
PARTITION BY sensor_id
ORDER BY bucket
) AS temperature_delta
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY bucket, sensor_id
ORDER BY sensor_id, bucket;
-- Calculate rate of change
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature,
rate(AVG(temperature), bucket) OVER (
PARTITION BY sensor_id
ORDER BY bucket
) AS temperature_rate
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY bucket, sensor_id
ORDER BY sensor_id, bucket;
7. Compression
Enable Compression
sql
-- Enable compression on hypertable
ALTER TABLE sensor_data SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'sensor_id',
timescaledb.compress_orderby = 'time'
);
-- View compression settings
SELECT *
FROM timescaledb_information.compression_settings
WHERE hypertable_name = 'sensor_data';
Manual Compression
sql
-- Compress a specific chunk
SELECT compress_chunk(
(SELECT chunk_name FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
AND range_start < NOW() - INTERVAL '7 days'
LIMIT 1)
);
-- Decompress a chunk
SELECT decompress_chunk(
(SELECT chunk_name FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
AND range_start < NOW() - INTERVAL '7 days'
LIMIT 1)
);
Compression Statistics
sql
-- View compression statistics
SELECT
chunk_schema,
chunk_name,
range_start,
range_end,
is_compressed,
pg_size_pretty(pg_total_relation_size(chunk_schema || '.' || chunk_name)) as size,
pg_size_pretty(pg_total_relation_size(chunk_schema || '.' || chunk_name || '_compressed')) as compressed_size
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_start;
-- View compression ratio
SELECT
hypertable_name,
COUNT(*) as total_chunks,
SUM(CASE WHEN is_compressed THEN 1 ELSE 0 END) as compressed_chunks,
ROUND(100.0 * SUM(CASE WHEN is_compressed THEN 1 ELSE 0 END) / COUNT(*), 2) as compression_percentage
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
GROUP BY hypertable_name;
8. Common Time-Series Queries
Aggregation Over Time
sql
-- Hourly aggregation
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature,
MIN(temperature) AS min_temperature,
MAX(temperature) AS max_temperature,
STDDEV(temperature) AS stddev_temperature,
COUNT(*) AS count
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY bucket, sensor_id
ORDER BY bucket;
-- Daily aggregation
SELECT
time_bucket('1 day', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature,
MIN(temperature) AS min_temperature,
MAX(temperature) AS max_temperature,
COUNT(*) AS count
FROM sensor_data
WHERE time >= NOW() - INTERVAL '30 days'
GROUP BY bucket, sensor_id
ORDER BY bucket;
Rolling Aggregations
sql
-- Rolling average using window functions
SELECT
time,
sensor_id,
temperature,
AVG(temperature) OVER (
PARTITION BY sensor_id
ORDER BY time
RANGE BETWEEN INTERVAL '1 hour' PRECEDING AND CURRENT ROW
) AS rolling_avg_1h
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
ORDER BY sensor_id, time;
-- Rolling sum
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
SUM(temperature) OVER (
PARTITION BY sensor_id
ORDER BY bucket
RANGE BETWEEN INTERVAL '24 hours' PRECEDING AND CURRENT ROW
) AS rolling_sum_24h
FROM sensor_data
WHERE time >= NOW() - INTERVAL '7 days'
GROUP BY bucket, sensor_id
ORDER BY sensor_id, bucket;
Percentile Calculations
sql
-- Using percentile_cont
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
percentile_cont(0.5) WITHIN GROUP (ORDER BY temperature) AS median_temperature,
percentile_cont(0.95) WITHIN GROUP (ORDER BY temperature) AS p95_temperature,
percentile_cont(0.99) WITHIN GROUP (ORDER BY temperature) AS p99_temperature
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY bucket, sensor_id
ORDER BY bucket;
-- Using percentile_disc
SELECT
time_bucket('1 day', time) AS bucket,
sensor_id,
percentile_disc(0.5) WITHIN GROUP (ORDER BY temperature) AS median_temperature,
percentile_disc(0.95) WITHIN GROUP (ORDER BY temperature) AS p95_temperature
FROM sensor_data
WHERE time >= NOW() - INTERVAL '30 days'
GROUP BY bucket, sensor_id
ORDER BY bucket;
Time-Series Joins
sql
-- Join sensor data with sensor metadata
SELECT
sd.time,
sd.sensor_id,
sm.name AS sensor_name,
sm.location,
sd.temperature,
sd.humidity
FROM sensor_data sd
JOIN sensor_metadata sm ON sd.sensor_id = sm.id
WHERE sd.time >= NOW() - INTERVAL '1 day'
ORDER BY sd.time;
-- Join with time bucket alignment
SELECT
time_bucket('1 hour', sd.time) AS bucket,
sd.sensor_id,
sm.name AS sensor_name,
AVG(sd.temperature) AS avg_temperature,
AVG(sd.humidity) AS avg_humidity
FROM sensor_data sd
JOIN sensor_metadata sm ON sd.sensor_id = sm.id
WHERE sd.time >= NOW() - INTERVAL '1 day'
GROUP BY bucket, sd.sensor_id, sm.name
ORDER BY bucket;
Anomaly Detection
sql
-- Simple anomaly detection using z-score
WITH stats AS (
SELECT
sensor_id,
AVG(temperature) AS avg_temp,
STDDEV(temperature) AS stddev_temp
FROM sensor_data
WHERE time >= NOW() - INTERVAL '7 days'
GROUP BY sensor_id
)
SELECT
sd.time,
sd.sensor_id,
sd.temperature,
s.avg_temp,
s.stddev_temp,
(sd.temperature - s.avg_temp) / s.stddev_temp AS z_score
FROM sensor_data sd
JOIN stats s ON sd.sensor_id = s.sensor_id
WHERE sd.time >= NOW() - INTERVAL '1 day'
AND ABS((sd.temperature - s.avg_temp) / s.stddev_temp) > 3
ORDER BY sd.time;
9. Downsampling Strategies
Basic Downsampling
sql
-- Create downsampled table
CREATE TABLE sensor_data_downsampled (
time TIMESTAMPTZ NOT NULL,
sensor_id INTEGER NOT NULL,
avg_temperature DOUBLE PRECISION,
min_temperature DOUBLE PRECISION,
max_temperature DOUBLE PRECISION,
avg_humidity DOUBLE PRECISION,
min_humidity DOUBLE PRECISION,
max_humidity DOUBLE PRECISION,
count BIGINT
);
-- Create hypertable
SELECT create_hypertable('sensor_data_downsampled', 'time');
-- Insert downsampled data
INSERT INTO sensor_data_downsampled
SELECT
time_bucket('1 hour', time) AS time,
sensor_id,
AVG(temperature) AS avg_temperature,
MIN(temperature) AS min_temperature,
MAX(temperature) AS max_temperature,
AVG(humidity) AS avg_humidity,
MIN(humidity) AS min_humidity,
MAX(humidity) AS max_humidity,
COUNT(*) AS count
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY time, sensor_id;
Downsampling with Continuous Aggregates
sql
-- Create continuous aggregate for downsampling
CREATE MATERIALIZED VIEW sensor_data_hourly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS time,
sensor_id,
AVG(temperature) AS avg_temperature,
MIN(temperature) AS min_temperature,
MAX(temperature) AS max_temperature,
STDDEV(temperature) AS stddev_temperature,
AVG(humidity) AS avg_humidity,
MIN(humidity) AS min_humidity,
MAX(humidity) AS max_humidity,
COUNT(*) AS count
FROM sensor_data
GROUP BY time, sensor_id;
-- Set refresh policy
SELECT add_continuous_aggregate_policy(
'sensor_data_hourly',
start_offset => INTERVAL '1 hour',
end_offset => INTERVAL '1 minute',
schedule_interval => INTERVAL '5 minutes'
);
Multi-level Downsampling
sql
-- Hourly level
CREATE MATERIALIZED VIEW sensor_data_hourly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS time,
sensor_id,
AVG(temperature) AS avg_temperature,
MIN(temperature) AS min_temperature,
MAX(temperature) AS max_temperature,
COUNT(*) AS count
FROM sensor_data
GROUP BY time, sensor_id;
-- Daily level (based on hourly)
CREATE MATERIALIZED VIEW sensor_data_daily
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', time) AS time,
sensor_id,
AVG(avg_temperature) AS avg_temperature,
MIN(min_temperature) AS min_temperature,
MAX(max_temperature) AS max_temperature,
SUM(count) AS count
FROM sensor_data_hourly
GROUP BY time, sensor_id;
-- Weekly level (based on daily)
CREATE MATERIALIZED VIEW sensor_data_weekly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 week', time) AS time,
sensor_id,
AVG(avg_temperature) AS avg_temperature,
MIN(min_temperature) AS min_temperature,
MAX(max_temperature) AS max_temperature,
SUM(count) AS count
FROM sensor_data_daily
GROUP BY time, sensor_id;
10. Monitoring and Alerting
Query Performance Monitoring
sql
-- Enable query stats
ALTER DATABASE timeseries_db SET timescaledb.enable_telemetry = on;
-- View query statistics
SELECT * FROM pg_stat_statements WHERE query LIKE '%sensor_data%';
-- View hypertable statistics
SELECT * FROM timescaledb_information.hypertable_size;
-- View chunk statistics
SELECT
chunk_schema,
chunk_name,
range_start,
range_end,
pg_size_pretty(pg_total_relation_size(chunk_schema || '.' || chunk_name)) as size
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_start;
Data Ingestion Monitoring
sql
-- Monitor data ingestion rate
SELECT
time_bucket('1 minute', time) AS bucket,
COUNT(*) AS rows_per_minute
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 hour'
GROUP BY bucket
ORDER BY bucket;
-- Monitor data by sensor
SELECT
sensor_id,
COUNT(*) AS row_count,
MIN(time) AS first_seen,
MAX(time) AS last_seen
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY sensor_id
ORDER BY row_count DESC;
Storage Monitoring
sql
-- View storage usage by hypertable
SELECT
hypertable_name,
pg_size_pretty(pg_total_relation_size(hypertable_schema || '.' || hypertable_name)) AS total_size,
pg_size_pretty(pg_total_relation_size(hypertable_schema || '.' || hypertable_name || '_compressed')) AS compressed_size
FROM timescaledb_information.hypertables;
-- View storage usage by chunk
SELECT
chunk_schema,
chunk_name,
range_start,
range_end,
is_compressed,
pg_size_pretty(pg_total_relation_size(chunk_schema || '.' || chunk_name)) AS size
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_start DESC
LIMIT 10;
Alert Queries
sql
-- Alert on missing data
SELECT
sensor_id,
MAX(time) AS last_data_time,
NOW() - MAX(time) AS time_since_last_data
FROM sensor_data
GROUP BY sensor_id
HAVING NOW() - MAX(time) > INTERVAL '1 hour'
ORDER BY time_since_last_data DESC;
-- Alert on anomalous values
WITH sensor_stats AS (
SELECT
sensor_id,
AVG(temperature) AS avg_temp,
STDDEV(temperature) AS stddev_temp
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY sensor_id
)
SELECT
sd.time,
sd.sensor_id,
sd.temperature,
ss.avg_temp,
ss.stddev_temp,
(sd.temperature - ss.avg_temp) / ss.stddev_temp AS z_score
FROM sensor_data sd
JOIN sensor_stats ss ON sd.sensor_id = ss.sensor_id
WHERE sd.time >= NOW() - INTERVAL '1 hour'
AND ABS((sd.temperature - ss.avg_temp) / ss.stddev_temp) > 3
ORDER BY sd.time;
11. Integration with Grafana
TimescaleDB Datasource Configuration
typescript
// Grafana datasource configuration
{
"name": "TimescaleDB",
"type": "postgres",
"url": "localhost:5432",
"database": "timeseries_db",
"user": "postgres",
"password": "password",
"sslMode": "disable",
"timescaledb": true
}
Grafana Query Examples
sql
-- Time series query for Grafana
SELECT
time_bucket('$__interval', time) AS time,
sensor_id,
AVG(temperature) AS avg_temperature
FROM sensor_data
WHERE time >= $__timeFrom() AND time <= $__timeTo()
AND sensor_id = $sensor_id
GROUP BY time, sensor_id
ORDER BY time;
-- Multi-sensor query
SELECT
time_bucket('$__interval', time) AS time,
sensor_id,
AVG(temperature) AS avg_temperature
FROM sensor_data
WHERE time >= $__timeFrom() AND time <= $__timeTo()
AND sensor_id IN ($sensor_ids)
GROUP BY time, sensor_id
ORDER BY time;
12. Best Practices
1. Choose Appropriate Chunk Intervals
sql
-- Good: Match chunk interval to data retention
SELECT create_hypertable(
'sensor_data',
'time',
chunk_time_interval => interval '1 day' -- For 30-day retention
);
-- Bad: Too small chunk interval causes overhead
SELECT create_hypertable(
'sensor_data',
'time',
chunk_time_interval => interval '1 minute' -- Too small
);
2. Use Continuous Aggregates for Pre-computed Data
sql
-- Good: Pre-compute aggregations
CREATE MATERIALIZED VIEW sensor_hourly_avg
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature
FROM sensor_data
GROUP BY bucket, sensor_id;
-- Bad: Compute aggregations on every query
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temperature
FROM sensor_data
WHERE time >= NOW() - INTERVAL '30 days'
GROUP BY bucket, sensor_id;
3. Use Compression for Historical Data
sql
-- Good: Compress old data
SELECT add_compression_policy(
'sensor_data',
INTERVAL '7 days',
schedule_interval => INTERVAL '1 day'
);
-- Bad: Keep all data uncompressed
4. Use Appropriate Time Buckets
sql
-- Good: Use appropriate bucket size for query
SELECT
time_bucket('1 hour', time) AS bucket,
AVG(temperature) AS avg_temperature
FROM sensor_data
WHERE time >= NOW() - INTERVAL '1 day'
GROUP BY bucket;
-- Bad: Use too small bucket for large time range
SELECT
time_bucket('1 second', time) AS bucket,
AVG(temperature) AS avg_temperature
FROM sensor_data
WHERE time >= NOW() - INTERVAL '30 days'
GROUP BY bucket;
5. Use Indexes Wisely
sql
-- Good: Create indexes on frequently queried columns CREATE INDEX idx_sensor_data_sensor_id ON sensor_data(sensor_id); CREATE INDEX idx_sensor_data_time ON sensor_data(time DESC); -- Bad: Create too many indexes
6. Monitor Performance
sql
-- Regularly monitor query performance SELECT * FROM pg_stat_statements WHERE query LIKE '%sensor_data%' ORDER BY total_time DESC LIMIT 10;
7. Use Partitioning for Large Datasets
sql
-- Good: Use multiple dimensions for partitioning
SELECT create_hypertable(
'iot_metrics',
'time',
partitioning_column => 'device_id',
number_partitions => 4
);
-- Bad: Use only time partitioning for multi-tenant data