| name | timescaledb |
| description | MANDATORY when working with time-series data, hypertables, continuous aggregates, or compression - enforces TimescaleDB 2.24.0 best practices including lightning-fast recompression, UUIDv7 continuous aggregates, and Direct Compress |
| allowed-tools | ["Read","Grep","Glob","Bash","mcp__github__*"] |
| model | opus |
TimescaleDB 2.24.0 Time-Series Database
Overview
TimescaleDB 2.24.0 introduces transformational features: lightning-fast recompression (100x faster updates), Direct Compress integration with continuous aggregates, UUIDv7 support in aggregates, and bloom filter sparse index changes. This skill ensures you leverage these capabilities correctly.
Core principle: Time-series data has unique access patterns. Design for append-heavy, time-range queries from the start.
Announce at start: "I'm applying timescaledb to ensure TimescaleDB 2.24.0 best practices."
When This Skill Applies
This skill is MANDATORY when ANY of these patterns are touched:
| Pattern | Examples |
|---|
**/*hypertable* | migrations/create_hypertable.sql |
**/*timeseries* | models/timeseries.ts |
**/*metrics* | services/metricsService.ts |
**/*events* | db/events.sql |
**/*logs* | tables/logs.sql |
**/*sensor* | iot/sensor_data.sql |
**/*continuous_agg* | views/hourly_stats.sql |
**/*compression* | policies/compression.sql |
Or when files contain:
create_hypertable
continuous aggregate
compress_chunk
add_compression_policy
TimescaleDB 2.24.0 Features
1. Lightning-Fast Recompression
TimescaleDB 2.24.0 introduces recompress := true for dramatically faster updates to compressed data:
UPDATE sensor_data
SET value = corrected_value
WHERE time BETWEEN '2026-01-01' AND '2026-01-02';
SELECT * FROM timescaledb_information.job_stats
WHERE job_id IN (
SELECT job_id FROM timescaledb_information.jobs
WHERE proc_name = 'policy_recompression'
);
When this matters:
- Late-arriving data corrections
- Backfill operations
- Data quality fixes
- Retroactive updates
2. Direct Compress with Continuous Aggregates
Continuous aggregates can now compress directly without materialized hypertable overhead:
CREATE MATERIALIZED VIEW hourly_metrics
WITH (timescaledb.continuous, timescaledb.compress = true) AS
SELECT
time_bucket('1 hour', time) AS bucket,
device_id,
avg(temperature) AS avg_temp,
min(temperature) AS min_temp,
max(temperature) AS max_temp,
count(*) AS sample_count
FROM sensor_readings
GROUP BY bucket, device_id
WITH NO DATA;
SELECT add_compression_policy('hourly_metrics', INTERVAL '7 days');
SELECT add_continuous_aggregate_policy('hourly_metrics',
start_offset => INTERVAL '1 month',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '1 hour'
);
Benefits:
- No intermediate materialized hypertable
- Automatic compression of aggregate data
- Reduced storage for historical aggregates
- Simpler management
3. UUIDv7 in Continuous Aggregates
TimescaleDB 2.24.0 supports PostgreSQL 18's native UUIDv7 in continuous aggregates:
CREATE TABLE events (
id uuid DEFAULT uuidv7(),
time timestamptz NOT NULL,
event_type text NOT NULL,
payload jsonb,
PRIMARY KEY (id, time)
);
SELECT create_hypertable('events', 'time');
CREATE MATERIALIZED VIEW event_counts
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS bucket,
event_type,
count(*) AS event_count,
count(DISTINCT id) AS unique_events
FROM events
GROUP BY bucket, event_type
WITH NO DATA;
4. Bloom Filter Sparse Index Changes
TimescaleDB 2.24.0 modifies bloom filter behavior for sparse indexes:
CREATE TABLE logs (
time timestamptz NOT NULL,
level text,
message text,
error_code text,
trace_id uuid
);
SELECT create_hypertable('logs', 'time');
ALTER TABLE logs SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'level',
timescaledb.compress_orderby = 'time DESC',
timescaledb.compress_bloomfilter = 'error_code, trace_id'
);
SELECT * FROM logs
WHERE error_code = 'E500'
AND time > now() - INTERVAL '1 day';
When to use bloom filters:
- Sparse columns (many NULLs)
- Rare value queries (finding errors in logs)
- High-cardinality exact match queries
- NOT useful for range queries
Hypertable Design
Creating Hypertables
CREATE TABLE metrics (
time timestamptz NOT NULL,
device_id uuid NOT NULL,
metric_name text NOT NULL,
value double precision,
metadata jsonb DEFAULT '{}'
);
SELECT create_hypertable('metrics', 'time',
chunk_time_interval => INTERVAL '1 day',
create_default_indexes => true
);
SELECT create_hypertable('metrics', 'time',
partitioning_column => 'device_id',
number_partitions => 4,
chunk_time_interval => INTERVAL '1 day'
);
Chunk Interval Selection
| Data Volume | Suggested Interval | Rationale |
|---|
| < 1GB/day | 1 week | Fewer chunks, simpler management |
| 1-10 GB/day | 1 day | Balance between size and granularity |
| 10-100 GB/day | 6 hours | Faster compression, better parallelism |
| > 100 GB/day | 1 hour | Maximum parallelism, fast drops |
SELECT set_chunk_time_interval('metrics', INTERVAL '6 hours');
SELECT show_chunks('metrics', older_than => INTERVAL '1 day');
Primary Key Design
CREATE TABLE events (
id uuid DEFAULT uuidv7(),
time timestamptz NOT NULL,
event_type text NOT NULL,
PRIMARY KEY (id, time)
);
CREATE TABLE events_bad (
id uuid PRIMARY KEY DEFAULT uuidv7(),
time timestamptz NOT NULL
);
Compression Strategy
Enabling Compression
ALTER TABLE metrics SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'device_id',
timescaledb.compress_orderby = 'time DESC',
timescaledb.compress_chunk_time_interval = '1 day'
);
SELECT compress_chunk(c)
FROM show_chunks('metrics', older_than => INTERVAL '7 days') c;
SELECT add_compression_policy('metrics', INTERVAL '7 days');
Segment By Selection
ALTER TABLE metrics SET (
timescaledb.compress_segmentby = 'device_id'
);
ALTER TABLE metrics SET (
timescaledb.compress_segmentby = 'device_id, metric_name'
);
ALTER TABLE events SET (
timescaledb.compress_segmentby = 'user_id'
);
ALTER TABLE events SET (
timescaledb.compress_segmentby = 'event_type',
timescaledb.compress_orderby = 'user_id, time DESC'
);
Order By Selection
ALTER TABLE metrics SET (
timescaledb.compress_orderby = 'time DESC'
);
ALTER TABLE logs SET (
timescaledb.compress_orderby = 'level, time DESC'
);
ALTER TABLE events SET (
timescaledb.compress_orderby = 'device_id, time DESC'
);
Continuous Aggregates
Creating Aggregates
CREATE MATERIALIZED VIEW hourly_stats
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS bucket,
device_id,
avg(value) AS avg_value,
min(value) AS min_value,
max(value) AS max_value,
count(*) AS sample_count
FROM metrics
GROUP BY bucket, device_id
WITH NO DATA;
CREATE MATERIALIZED VIEW daily_stats
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', bucket) AS bucket,
device_id,
avg(avg_value) AS avg_value,
min(min_value) AS min_value,
max(max_value) AS max_value,
sum(sample_count) AS sample_count
FROM hourly_stats
GROUP BY 1, device_id
WITH NO DATA;
Refresh Policies
SELECT add_continuous_aggregate_policy('hourly_stats',
start_offset => INTERVAL '3 days',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '1 hour'
);
ALTER MATERIALIZED VIEW hourly_stats SET (
timescaledb.materialized_only = false
);
CALL refresh_continuous_aggregate('hourly_stats',
'2026-01-01'::timestamptz,
'2026-01-02'::timestamptz
);
With Compression (2.24.0)
CREATE MATERIALIZED VIEW hourly_metrics
WITH (
timescaledb.continuous,
timescaledb.compress = true
) AS
SELECT
time_bucket('1 hour', time) AS bucket,
device_id,
avg(temperature) AS avg_temp,
percentile_agg(temperature) AS temp_pct
FROM sensor_readings
GROUP BY bucket, device_id
WITH NO DATA;
SELECT add_compression_policy('hourly_metrics', INTERVAL '30 days');
SELECT add_continuous_aggregate_policy('hourly_metrics',
start_offset => INTERVAL '7 days',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '1 hour'
);
Retention Policies
Data Lifecycle
SELECT add_retention_policy('metrics', INTERVAL '90 days');
SELECT * FROM timescaledb_information.jobs
WHERE proc_name = 'policy_retention';
SELECT remove_retention_policy('metrics');
Tiered Storage Pattern
SELECT add_compression_policy('metrics', INTERVAL '7 days');
SELECT add_retention_policy('metrics', INTERVAL '90 days');
SELECT add_compression_policy('hourly_stats', INTERVAL '30 days');
SELECT add_retention_policy('hourly_stats', INTERVAL '1 year');
SELECT add_compression_policy('daily_stats', INTERVAL '90 days');
Query Patterns
Time Range Queries
SELECT * FROM metrics
WHERE time > now() - INTERVAL '1 hour'
AND device_id = $1
ORDER BY time DESC
LIMIT 100;
SELECT
time_bucket('5 minutes', time) AS bucket,
avg(value) AS avg_value
FROM metrics
WHERE time BETWEEN $1 AND $2
AND device_id = $3
GROUP BY bucket
ORDER BY bucket;
SELECT DISTINCT ON (device_id)
device_id,
time,
value
FROM metrics
WHERE time > now() - INTERVAL '1 day'
ORDER BY device_id, time DESC;
Using Continuous Aggregates
SELECT * FROM hourly_stats
WHERE bucket > now() - INTERVAL '7 days'
AND device_id = $1
ORDER BY bucket DESC;
SELECT * FROM hourly_stats
WHERE bucket > now() - INTERVAL '1 hour';
Percentiles and Statistics
CREATE MATERIALIZED VIEW metrics_percentiles
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS bucket,
device_id,
percentile_agg(value) AS value_pct,
stats_agg(value) AS value_stats
FROM metrics
GROUP BY bucket, device_id;
SELECT
bucket,
device_id,
approx_percentile(0.50, value_pct) AS median,
approx_percentile(0.95, value_pct) AS p95,
approx_percentile(0.99, value_pct) AS p99,
average(value_stats) AS avg,
stddev(value_stats) AS stddev
FROM metrics_percentiles
WHERE bucket > now() - INTERVAL '24 hours';
Index Strategy
Default Indexes
CREATE INDEX idx_metrics_device_time ON metrics (device_id, time DESC);
CREATE INDEX idx_metrics_errors ON metrics (time DESC)
WHERE value > threshold;
Compressed Chunk Considerations
ALTER TABLE metrics SET (
timescaledb.compress_segmentby = 'device_id',
timescaledb.compress_orderby = 'time DESC'
);
Migration Patterns
Converting Regular Table to Hypertable
ALTER TABLE legacy_metrics ALTER COLUMN time SET NOT NULL;
SELECT create_hypertable('legacy_metrics', 'time',
migrate_data => true,
chunk_time_interval => INTERVAL '1 day'
);
ALTER TABLE legacy_metrics SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'device_id',
timescaledb.compress_orderby = 'time DESC'
);
SELECT add_compression_policy('legacy_metrics', INTERVAL '7 days');
SELECT add_retention_policy('legacy_metrics', INTERVAL '90 days');
Adding TimescaleDB to Existing Database
CREATE EXTENSION IF NOT EXISTS timescaledb;
SELECT extversion FROM pg_extension WHERE extname = 'timescaledb';
SELECT timescaledb_information.version();
TimescaleDB Artifact
When implementing time-series features, post this artifact:
<!-- TIMESCALEDB_IMPLEMENTATION:START -->
## TimescaleDB Implementation Summary
### Hypertables
| Table | Chunk Interval | Space Partitions | Compression |
|-------|----------------|------------------|-------------|
| metrics | 1 day | device_id (4) | Yes |
| events | 6 hours | None | Yes |
| logs | 1 hour | level (2) | Yes |
### Compression Settings
| Table | Segment By | Order By | Bloom Filter |
|-------|------------|----------|--------------|
| metrics | device_id | time DESC | None |
| logs | level | time DESC | error_code, trace_id |
### Continuous Aggregates
| Aggregate | Source | Interval | Compression |
|-----------|--------|----------|-------------|
| hourly_metrics | metrics | 1 hour | Yes (30d) |
| daily_metrics | hourly_metrics | 1 day | Yes (90d) |
### Policies
| Table/Aggregate | Compression | Retention | Refresh |
|-----------------|-------------|-----------|---------|
| metrics | 7 days | 90 days | N/A |
| hourly_metrics | 30 days | 1 year | 1 hour |
| daily_metrics | 90 days | Never | 1 day |
### TimescaleDB 2.24.0 Features Used
- [ ] Lightning-fast recompression
- [ ] Direct Compress with continuous aggregates
- [ ] UUIDv7 in continuous aggregates
- [ ] Bloom filter sparse indexes
**TimescaleDB Version:** 2.24.0
**Verified At:** [timestamp]
<!-- TIMESCALEDB_IMPLEMENTATION:END -->
Checklist
Before completing TimescaleDB implementation:
Integration
This skill integrates with:
database-architecture - Hypertables follow general schema patterns
postgres-rls - RLS works with hypertables (use caution with compression)
postgis - Spatial time-series data
References