> Designing append-heavy tables for metrics, events, and logs with time-based partitioning, retention policies, and efficient aggregation.
Scanned 9/11/2026
Install to Claude Code
npx -y skills add Intense-Visions/harness-engineering --skill db-time-series --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Db Time Series?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/intense-visions-db-time-series-93d07b9e)More formats (shields.io, HTML) on the badges page.
# Time-Series Data
> Designing append-heavy tables for metrics, events, and logs with time-based partitioning, retention policies, and efficient aggregation.
## When to Use
- IoT sensor data (temperature, pressure, GPS coordinates)
- Application metrics and observability data (request latency, error rates)
- Financial tick data and market prices
- Server logs, access logs, audit events
- Any workload that is append-heavy with time-ordered queries and eventual data expiry
## Instructions
### Key Concepts
Time-series workloads share four characteristics:
1. **Append-only or append-mostly** -- rows are inserted, rarely updated or deleted
2. **Time-range queries** -- WHERE clauses filter by time window
3. **Recency bias** -- recent data is queried far more than old data
4. **Retention period** -- data expires after days, months, or years
**Schema design with partitioning:**
```sql
CREATE TABLE metrics (
time timestamptz NOT NULL,
device_id int NOT NULL,
temperature float,
humidity float
) PARTITION BY RANGE (time);
-- Create monthly partitions
CREATE TABLE metrics_2024_01 PARTITION OF metrics
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE metrics_2024_02 PARTITION OF metrics
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
```
**Key design choices:**
- **Partition interval:** Match query granularity. Daily partitions for dashboards, monthly for reports.
- **Chunk size:** Target each partition at 25% of available memory for cache efficiency.
- **Index strategy:** BRIN index on time (much smaller than B-tree for sequential data). B-tree composite on `(device_id, time)` for per-device queries.
- **Retention:** `DROP TABLE metrics_2024_01;` is instant. `DELETE WHERE time < X` creates dead tuples requiring vacuum -- avoid this.
### Worked Example
IoT sensor monitoring platform:
```sql
-- BRIN index on time (100x smaller than B-tree for sequential inserts)
CREATE INDEX idx_metrics_time_brin ON metrics USING brin (time);
-- Composite B-tree for per-device queries
CREATE INDEX idx_metrics_device_time ON metrics (device_id, time);
```
**Time-range query with partition pruning:**
```sql
EXPLAIN ANALYZE
SELECT device_id, avg(temperature), max(humidity)
FROM metrics
WHERE time BETWEEN '2024-03-01' AND '2024-03-31'
GROUP BY device_id;
```
The query plan shows `Partitions removed: 10` -- only the March partition is scanned. Without partitioning, the entire table would be read.
**Retention by dropping old partitions:**
```sql
-- Instant operation, no dead tuples, no vacuum needed
DROP TABLE metrics_2023_01;
```
Compare with DELETE-based retention: `DELETE FROM metrics WHERE time < '2023-02-01'` on a 500M-row table generates millions of dead tuples, triggers autovacuum storms, and takes hours.
**Continuous aggregation for dashboards:**
```sql
CREATE MATERIALIZED VIEW hourly_metrics AS
SELECT
date_trunc('hour', time) AS hour,
device_id,
avg(temperature) AS avg_temp,
max(temperature) AS max_temp,
count(*) AS readings
FROM metrics
GROUP BY 1, 2;
CREATE UNIQUE INDEX ON hourly_metrics (hour, device_id);
-- Refresh periodically
REFRESH MATERIALIZED VIEW CONCURRENTLY hourly_metrics;
```
### Anti-Patterns
1. **Single unpartitioned table for time-series.** Leads to bloat, vacuum pressure, and slow range queries as the table grows past 100M rows.
2. **DELETE for retention instead of DROP PARTITION.** Creates dead tuples, triggers vacuum overhead, and can take hours on large tables.
3. **B-tree index on time column for sequential append data.** BRIN indexes are 100x smaller and provide comparable query performance for time-range scans on physically ordered data.
4. **Not aligning partition boundaries with query patterns.** Monthly partitions with daily queries waste I/O. Daily partitions with yearly queries create thousands of partitions.
5. **Updating time-series rows.** Breaks the append-only assumption, causes HOT update failures on BRIN-indexed tables, and degrades partition pruning effectiveness.
### PostgreSQL Specifics
**Declarative partitioning** (`PARTITION BY RANGE`) is the foundation. PostgreSQL 11+ supports partition pruning at plan time and execution time.
**BRIN indexes** are ideal for time columns because data is physically ordered by insertion time:
```sql
CREATE INDEX ON metrics USING brin (time) WITH (pages_per_range = 32);
```
The `pages_per_range` parameter controls granularity -- smaller values give tighter range summaries at the cost of a larger index.
**pg_partman extension** automates partition creation and retention:
```sql
SELECT partman.create_parent(
'public.metrics', 'time', 'native', 'monthly'
);
-- Automatically creates future partitions and drops old ones
```
**Materialized views** with `REFRESH CONCURRENTLY` for zero-downtime dashboard aggregation. Requires a unique index on the materialized view.
## Details
### Advanced Topics
**TimescaleDB hypertables** automatically chunk data by time, eliminating manual partition management:
```sql
SELECT create_hypertable('metrics', 'time');
```
TimescaleDB adds compression (columnar, 90%+ size reduction on old chunks), continuous aggregates (real-time aggregation combining materialized and recent data), and `time_bucket()` for flexible time grouping.
**Two-step aggregation:** Pre-aggregate in a materialized view (hourly), then re-aggregate at query time (daily, weekly). This reduces the data scanned for dashboard queries by 100-1000x.
**Write-ahead log tuning** for high-throughput ingestion:
- `synchronous_commit = off` for metrics where losing a few seconds of data on crash is acceptable
- `wal_level = minimal` if replication is not needed
- Batch inserts with `COPY` instead of individual INSERTs (10-50x throughput improvement)
### Engine Differences
MySQL 8.0 supports `PARTITION BY RANGE` but with key differences:
- MySQL lacks BRIN indexes -- use B-tree on time column (larger but functional)
- MySQL partitioning has a limit of 8192 partitions per table
- MySQL lacks materialized views entirely -- use summary tables with scheduled events (`CREATE EVENT`) or application-level refresh
- MySQL does not have a TimescaleDB equivalent -- consider ClickHouse or InfluxDB for high-volume time-series workloads on MySQL stacks
For MySQL, the partition management pattern:
```sql
ALTER TABLE metrics DROP PARTITION p_2023_01; -- instant retention
ALTER TABLE metrics ADD PARTITION (
PARTITION p_2024_04 VALUES LESS THAN ('2024-05-01')
);
```
### Real-World Case Studies
**Fleet management platform ingesting 500K sensor readings/minute from 100K vehicles.** Partitioned by day (30 partitions retained), BRIN index on time, composite B-tree on `(vehicle_id, time)`. Write throughput: 500K rows/minute sustained using batched COPY. Query latency: "last 24h for vehicle X" in 4ms. Retention: daily cron drops partitions older than 30 days -- instant, no vacuum. Storage: 2TB raw data per month, reduced to 200GB with TimescaleDB compression on chunks older than 7 days.
## Source
- [TimescaleDB Documentation](https://docs.timescale.com/use-timescale/latest/hypertables/)
- [PostgreSQL Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html)
- [PostgreSQL BRIN Indexes](https://www.postgresql.org/docs/current/brin-intro.html)
## Process
1. Read the key concepts to understand time-series characteristics and partitioning strategy.
2. Apply time-based partitioning with BRIN indexes and partition-drop retention.
3. Verify with EXPLAIN ANALYZE that partition pruning is active and that BRIN index scans are used for time-range queries.
## Harness Integration
- **Type:** knowledge -- this skill is a reference document, not a procedural workflow.
- **No tools or state** -- consumed as context by other skills and agents.
- **related_skills:** db-temporal-data, db-audit-trail, db-btree-index, db-composite-index
## Success Criteria
- Time-series tables use time-based partitioning with BRIN indexes.
- Retention is implemented via partition drop, not DELETE.
- Aggregation uses materialized views or continuous aggregates for dashboard queries.
- EXPLAIN ANALYZE confirms partition pruning and BRIN index usage.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!