Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use this skill to analyze an existing PostgreSQL database and identify which tables should be converted to Timescale/TimescaleDB hypertables. **Trigger when user asks to:** - Analyze database tables for hypertable conversion potential - Identify time-series or event tables in an existing schema - Evaluate if a table would benefit from Timescale/TimescaleDB - Audit PostgreSQL tables for migration to Timescale/TimescaleDB/TigerData - Score or rank tables for hypertable candidacy **Keywords:** h
.claude/skills/timescale-find-hypertable-candidates/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 49% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 37% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 44% | 0% |
Identify tables that would benefit from TimescaleDB hypertable conversion. After identification, use the companion "migrate-postgres-tables-to-hypertables" skill for configuration and migration.
Performance gains: 90%+ compression, fast time-based queries, improved insert performance, efficient aggregations, continuous aggregates for materialization (dashboards, reports, analytics), automatic data management (retention, compression).
Best for insert-heavy patterns:
Requirements: Large volumes (1M+ rows), time-based queries, infrequent updates
sql-- Get all tables with row counts and insert/update patterns WITH table_stats AS ( SELECT schemaname, tablename, n_tup_ins as total_inserts, n_tup_upd as total_updates, n_tup_del as total_deletes, n_live_tup as live_rows, n_dead_tup as dead_rows FROM pg_stat_user_tables ), table_sizes AS ( SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size, pg_total_relation_size(schemaname||'.'||tablename) as total_size_bytes FROM pg_tables WHERE schemaname NOT IN ('information_schema', 'pg_catalog') ) SELECT ts.schemaname, ts.tablename, ts.live_rows, tsize.total_size, tsize.total_size_bytes, ts.total_inserts, ts.total_updates, ts.total_deletes, ROUND(CASE WHEN ts.live_rows > 0 THEN (ts.total_inserts::float / ts.live_rows) * 100 ELSE 0 END, 2) as insert_ratio_pct FROM table_stats ts JOIN table_sizes tsize ON ts.schemaname = tsize.schemaname AND ts.tablename = tsize.tablename ORDER BY tsize.total_size_bytes DESC;
Look for:
sql-- Identify common query dimensions SELECT schemaname, tablename, indexname, indexdef FROM pg_indexes WHERE schemaname NOT IN ('information_schema', 'pg_catalog') ORDER BY tablename, indexname;
Look for:
sql-- Check availability SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'); -- Analyze expensive queries for candidate tables SELECT query, calls, mean_exec_time, total_exec_time FROM pg_stat_statements WHERE query ILIKE '%your_table_name%' ORDER BY total_exec_time DESC LIMIT 20;
✅ Good patterns: Time-based WHERE, entity filtering combined with time-based qualifiers, GROUP BY time_bucket, range queries over time ❌ Poor patterns: Non-time lookups with no time-based qualifiers in same query (WHERE email = ...)
sql-- Check migration compatibility SELECT conname, contype, pg_get_constraintdef(oid) as definition FROM pg_constraint WHERE conrelid = 'your_table_name'::regclass;
Compatibility:
python# Append-only logging INSERT INTO events (user_id, event_time, data) VALUES (...); # Time-series collection INSERT INTO metrics (device_id, timestamp, value) VALUES (...); # Time-based queries SELECT * FROM metrics WHERE timestamp >= NOW() - INTERVAL '24 hours'; # Time aggregations SELECT DATE_TRUNC('day', timestamp), COUNT(*) GROUP BY 1;
python# Frequent updates to historical records UPDATE users SET email = ..., updated_at = NOW() WHERE id = ...; # Non-time lookups SELECT * FROM users WHERE email = ...; # Small reference tables SELECT * FROM countries ORDER BY name;
✅ GOOD:
❌ POOR:
Sequential ID tables can be candidates if:
sqlCREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, -- Can partition by ID user_id BIGINT, created_at TIMESTAMPTZ DEFAULT NOW() -- For sparse indexes );
Note: For ID-based tables where there is also a time column (created_at, ordered_at, etc.), you can partition by ID and use sparse indexes on the time column. See the migrate-postgres-tables-to-hypertables skill for details.
✅ Event/Log Tables (user_events, audit_logs)
sqlCREATE TABLE user_events ( id BIGSERIAL PRIMARY KEY, user_id BIGINT, event_type TEXT, event_time TIMESTAMPTZ DEFAULT NOW(), metadata JSONB ); -- Partition by id, segment by user_id, enable minmax sparse_index on event_time
✅ Sensor/IoT Data (sensor_readings, telemetry)
sqlCREATE TABLE sensor_readings ( device_id TEXT, timestamp TIMESTAMPTZ, temperature DOUBLE PRECISION, humidity DOUBLE PRECISION ); -- Partition by timestamp, segment by device_id, minmax sparse indexes on temperature and humidity
✅ Financial/Trading (stock_prices, transactions)
sqlCREATE TABLE stock_prices ( symbol VARCHAR(10), price_time TIMESTAMPTZ, open_price DECIMAL, close_price DECIMAL, volume BIGINT ); -- Partition by price_time, segment by symbol, minmax sparse indexes on open_price and close_price and volume
✅ System Metrics (monitoring_data)
sqlCREATE TABLE system_metrics ( hostname TEXT, metric_time TIMESTAMPTZ, cpu_usage DOUBLE PRECISION, memory_usage BIGINT ); -- Partition by metric_time, segment by hostname, minmax sparse indexes on cpu_usage and memory_usage
❌ Reference Tables (countries, categories)
sqlCREATE TABLE countries ( id SERIAL PRIMARY KEY, name VARCHAR(100), code CHAR(2) ); -- Static data, no time component
❌ User Profiles (users, accounts)
sqlCREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255), created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ ); -- Accessed by ID, frequently updated, has timestamp but it's not the primary query dimension (the primary query dimension is id or email)
❌ Settings/Config (user_settings)
sqlCREATE TABLE user_settings ( user_id BIGINT PRIMARY KEY, theme VARCHAR(20), -- Changes: light -> dark -> auto language VARCHAR(10), -- Changes: en -> es -> fr notifications JSONB, -- Frequent preference updates updated_at TIMESTAMPTZ ); -- Accessed by user_id, frequently updated, has timestamp but it's not the primary query dimension (the primary query dimension is user_id)
For each candidate table provide:
Focus on insert-heavy patterns with time-based or sequential access. Tables scoring 8+ points are strong candidates for conversion.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | fail→pass | 15,276 | 7,817 | -49% | 1 | 1 | 0% | 2,679 | 3,993 | +49% | 0 | 0 | — |
case-01 | fail→fail | 22,969 | 3,625 | -84% | 1 | 1 | 0% | 4,312 | 3,217 | -25% | 0 | 0 | — |
case-03 | fail→fail | 19,340 | 9,203 | -52% | 1 | 1 | 0% | 3,581 | 4,253 | +19% | 0 | 0 | — |
case-04 | fail→fail | 12,807 | 10,301 | -20% | 1 | 1 | 0% | 2,433 | 4,353 | +79% | 0 | 0 | — |
case-05 | pass→pass | 8,494 | 6,080 | -28% | 1 | 1 | 0% | 1,618 | 3,729 | +130% | 0 | 0 | — |
case-06 | pass→pass | 4,354 | 4,445 | +2% | 1 | 1 | 0% | 822 | 3,391 | +313% | 0 | 0 | — |
case-07 | fail→pass | 11,196 | 3,855 | -66% | 1 | 1 | 0% | 1,946 | 3,250 | +67% | 0 | 0 | — |
case-08 | fail→pass | 11,061 | 1,912 | -83% | 1 | 1 | 0% | 1,869 | 2,826 | +51% | 0 | 0 | — |
case-09 | fail→pass | 11,068 | 2,153 | -81% | 1 | 1 | 0% | 2,040 | 2,789 | +37% | 0 | 0 | — |
case-10 | fail→pass | 12,620 | 3,061 | -76% | 1 | 1 | 0% | 2,079 | 2,998 | +44% | 0 | 0 | — |
case-11 | pass→pass | 4,144 | 5,306 | +28% | 1 | 1 | 0% | 716 | 3,484 | +387% | 0 | 0 | — |
case-12 | fail→pass | 13,479 | 9,888 | -27% | 1 | 1 | 0% | 2,377 | 4,277 | +80% | 0 | 0 | — |
case-13 | fail→pass | 20,423 | 5,571 | -73% | 1 | 1 | 0% | 1,226 | 3,723 | +204% | 0 | 0 | — |
case-14 | pass→pass | 10,373 | 8,002 | -23% | 1 | 1 | 0% | 1,911 | 3,891 | +104% | 0 | 0 | — |
case-15 | fail→fail | 14,562 | 8,762 | -40% | 1 | 1 | 0% | 2,914 | 4,214 | +45% | 0 | 0 | — |
case-16 | pass→pass | 14,242 | 6,275 | -56% | 1 | 1 | 0% | 2,555 | 3,779 | +48% | 0 | 0 | — |
case-17 | pass→pass | 14,159 | 8,926 | -37% | 1 | 1 | 0% | 2,338 | 4,067 | +74% | 0 | 0 | — |
case-18 | pass→pass | 13,169 | 7,778 | -41% | 1 | 1 | 0% | 1,910 | 3,786 | +98% | 0 | 0 | — |
case-19 | pass→pass | 8,186 | 5,110 | -38% | 1 | 1 | 0% | 1,350 | 3,138 | +132% | 0 | 0 | — |
case-20 | fail→pass | 9,353 | 1,416 | -85% | 1 | 1 | 0% | 1,447 | 2,676 | +85% | 0 | 0 | — |
case-21 | pass→pass | 10,604 | 5,347 | -50% | 1 | 1 | 0% | 1,820 | 3,441 | +89% | 0 | 0 | — |
case-22 | pass→pass | 9,540 | 7,774 | -19% | 1 | 1 | 0% | 1,575 | 3,912 | +148% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +36 percentage points is the difference between those two pass rates over the 21 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.