TimescaleDB Complete Setup
Instructions for insert-heavy data patterns where data is inserted but rarely changed:
- Time-series data (sensors, metrics, system monitoring)
- Event logs (user events, audit trails, application logs)
- Transaction records (orders, payments, financial transactions)
- Sequential data (records with auto-incrementing IDs and timestamps)
- Append-only datasets (immutable records, historical data)
Step 1: Create Hypertable
Compression Decision
- Enable by default for insert-heavy patterns
- Disable if table has vector type columns (pgvector) - indexes on vector columns incompatible with columnstore
Partition Column Selection
Must be time-based (TIMESTAMP/TIMESTAMPTZ/DATE) or integer (INT/BIGINT) with good temporal/sequential distribution.
Common patterns:
- TIME-SERIES:
timestamp,event_time,measured_at - EVENT LOGS:
event_time,created_at,logged_at - TRANSACTIONS:
created_at,transaction_time,processed_at - SEQUENTIAL:
id(auto-increment when no timestamp),sequence_number - APPEND-ONLY:
created_at,inserted_at,id
Less ideal: ingested_at (when data entered system - use only if it's your primary query dimension)
Avoid: updated_at (breaks time ordering unless it's primary query dimension)
Segment_By Column Selection
PREFER SINGLE COLUMN - multi-column rarely optimal. Multi-column can only work for highly correlated columns (e.g., metric_name + metric_type) with sufficient row density.
Requirements:
- Frequently used in WHERE clauses (most common filter)
- Good row density (>100 rows per value per chunk)
- Primary logical partition/grouping
Examples:
- IoT:
device_id - Finance:
symbol - Metrics:
service_name,service_name, metric_type(if sufficient row density),metric_name, metric_type(if sufficient row density) - Analytics:
user_idif sufficient row density, otherwisesession_id - E-commerce:
product_idif sufficient row density, otherwisecategory_id
Row density guidelines:
- Target: >100 rows per segment_by value within each chunk.
- Poor: <10 rows per segment_by value per chunk → choose less granular column
- What to do with low-density columns: prepend to order_by column list.
Query pattern drives choice:
Avoid: timestamps, unique IDs, low-density columns (<100 rows/value/chunk), columns rarely used in filtering
Order_By Column Selection
Creates natural time-series progression when combined with segment_by for optimal compression.
Most common: timestamp DESC
Examples:
- IoT/Finance/E-commerce:
timestamp DESC - Metrics:
metric_name, timestamp DESC(if metric_name has too low density for segment_by) - Analytics:
user_id, timestamp DESC(user_id has too low density for segment_by)
Alternative patterns:
sequence_id DESCfor event streams with sequence numberstimestamp DESC, event_order DESCfor sub-ordering within same timestamp
Low-density column handling: If a column has <100 rows per chunk (too low for segment_by), prepend it to order_by:
- Example:
metric_namehas 20 rows/chunk → usesegment_by='service_name',order_by='metric_name, timestamp DESC' - Groups similar values together (all temperature readings, then pressure readings) for better compression
Good test: ordering created by (segment_by_column, order_by_column) should form a natural time-series progression. Values close to each other in the progression should be similar.
Avoid in order_by: random columns, columns with high variance between adjacent rows, columns unrelated to segment_by
Compression Sparse Index Selection
Sparse indexes enable query filtering on compressed data without decompression. Store metadata per batch (~1000 rows) to eliminate batches that don't match query predicates.
Types:
- minmax: Min/max values per batch - for range queries (>, <, BETWEEN) on numeric/temporal columns
Use minmax for: price, temperature, measurement, timestamp (range filtering)
Use for:
- minmax for outlier detection (temperature > 90).
- minmax for fields that are highly correlated with segmentby and orderby columns (e.g. if orderby includes
created_at, minmax onupdated_atis useful).
Avoid: rarely filtered columns.
IMPORTANT: NEVER index columns in segmentby or orderby. Orderby columns will always have minmax indexes without any configuration.
Configuration: The format is a comma-separated list of type_of_index(column_name).
Explicit configuration available since v2.22.0 (was auto-created since v2.16.0).
Chunk Time Interval (Optional)
Default: 7 days (use if volume unknown, or ask user). Adjust based on volume:
- High frequency: 1 hour - 1 day
- Medium: 1 day - 1 week
- Low: 1 week - 1 month
Good test: recent chunk indexes should fit in less than 25% of RAM.
Indexes & Primary Keys
Common index patterns - composite indexes on an id and timestamp:
Important: Only create indexes you'll actually use - each has maintenance overhead.
Primary key and unique constraints rules: Must include partition column.
Option 1: Composite PK with partition column
Option 2: Single-column PK (only if it's the partition column)
Option 3: No PK: strict uniqueness is often not required for insert-heavy patterns.
Step 2: Compression Policy (Optional)
IMPORTANT: If you used tsdb.enable_columnstore=true in Step 1, starting with TimescaleDB version 2.23 a columnstore policy is automatically created with after => INTERVAL '7 days'. You only need to call add_columnstore_policy() if you want to customize the after interval to something other than 7 days.
Set after interval for when: data becomes mostly immutable (some updates/backfill OK) AND B-tree indexes aren't needed for queries (less common criterion).
Step 3: Retention Policy
IMPORTANT: Don't guess - ask user or comment out if unknown.
Step 4: Create Continuous Aggregates
Use different aggregation intervals for different uses.
Short-term (Minutes/Hours)
For up-to-the-minute dashboards on high-frequency data.
Long-term (Days/Weeks/Months)
For long-term reporting and analytics.
Step 5: Aggregate Refresh Policies
Set up refresh policies based on your data freshness requirements.
start_offset: Usually omit (refreshes all). Exception: If you don't care about refreshing data older than X (see below). With retention policy on raw data: match the retention policy.
end_offset: Set beyond active update window (e.g., 15 min if data usually arrives within 10 min). Data newer than end_offset won't appear in queries without real-time aggregation. If you don't know your update window, use the size of the time_bucket in the query, but not less than 5 minutes.
schedule_interval: Set to the same value as the end_offset but not more than 1 hour.
Hourly - frequent refresh for dashboards:
Daily - less frequent for reports:
Use start_offset only if you don't care about refreshing old data Use for high-volume systems where query accuracy on older data doesn't matter:
IMPORTANT: you MUST set a start_offset to be less than the retention policy on raw data. By default, set the start_offset equal to the retention policy. If the retention policy is commented out, comment out the start_offset as well. like this:
Step 6: Real-Time Aggregation (Optional)
Real-time combines materialized + recent raw data at query time. Provides up-to-date results at the cost of higher query latency.
More useful for fine-grained aggregates (e.g., minutely) than coarse ones (e.g., daily/monthly) since large buckets will be mostly incomplete with recent data anyway.
Disabled by default in v2.13+, before that it was enabled by default.
Use when: Need data newer than end_offset, up-to-minute dashboards, can tolerate higher query latency Disable when: Performance critical, refresh policies sufficient, high query volume, missing and stale data for recent data is acceptable
Enable for current results (higher query cost):
Disable for performance (but with stale results):
Step 7: Compress Aggregates
Rule: segment_by = ALL GROUP BY columns except time_bucket, order_by = time_bucket DESC
Step 8: Aggregate Retention
Aggregates are typically kept longer than raw data. IMPORTANT: Don't guess - ask user or you MUST comment out if unknown.
Step 9: Performance Indexes on Continuous Aggregates
Index strategy: Analyze WHERE clauses in common queries → Create indexes matching filter columns + time ordering
Pattern: (filter_column, bucket DESC) supports WHERE filter_column = X AND bucket >= Y ORDER BY bucket DESC
Examples:
Multi-column filters: Create composite indexes for WHERE entity_id = X AND category = Y:
Important: Only create indexes you'll actually use - each has maintenance overhead.
Step 10: Optional Enhancements
Space Partitioning (NOT RECOMMENDED)
Only for query patterns where you ALWAYS filter by the space-partition column with expert knowledge and extensive benchmarking. STRONGLY prefer time-only partitioning.
Step 11: Verify Configuration
Performance Guidelines
- Chunk size: Recent chunk indexes should fit in less than 25% of RAM
- Compression: Expect 90%+ reduction (10x) with proper columnstore config
- Query optimization: Use continuous aggregates for historical queries and dashboards
- Memory: Run
timescaledb-tunefor self-hosting (auto-configured on cloud)
Schema Best Practices
Do's and Don'ts
- ✅ Use
TIMESTAMPTZNOTtimestamp - ✅ Use
>=and<NOTBETWEENfor timestamps - ✅ Use
TEXTwith constraints NOTchar(n)/varchar(n) - ✅ Use
snake_caseNOTCamelCase - ✅ Use
BIGINT GENERATED ALWAYS AS IDENTITYNOTSERIAL - ✅ Use
BIGINTfor IDs by default overINTEGERorSMALLINT - ✅ Use
DOUBLE PRECISIONby default overREAL/FLOAT - ✅ Use
NUMERICNOTMONEY - ✅ Use
NOT EXISTSNOTNOT IN - ✅ Use
time_bucket()ordate_trunc()NOTtimestamp(0)for truncation
API Reference (Current vs Deprecated)
Deprecated Parameters → New Parameters:
timescaledb.compress→timescaledb.enable_columnstoretimescaledb.compress_segmentby→timescaledb.segmentbytimescaledb.compress_orderby→timescaledb.orderby
Deprecated Functions → New Functions:
add_compression_policy()→add_columnstore_policy()remove_compression_policy()→remove_columnstore_policy()compress_chunk()→convert_to_columnstore()(use withCALL, notSELECT)decompress_chunk()→convert_to_rowstore()(use withCALL, notSELECT)
Compression Stats (use functions, not views):
- Use function:
hypertable_compression_stats('table_name') - Use function:
chunk_compression_stats('_timescaledb_internal._hyper_X_Y_chunk') - Note: Views like
columnstore_settingsmay not be available in all versions; use functions instead
Manual Compression Example:
Questions to Ask User
- What kind of data will you be storing?
- How do you expect to use the data?
- What queries will you run?
- How long to keep the data?
- Column types if unclear

