Skip to content
Back to skills

Postgresql Optimization

ASecurity

PostgreSQL optimization — query tuning, indexing, EXPLAIN analysis, configuration, maintenance, and monitoring. Use when slow queries, missing indexes, vacuum lag, or performance bottlenecks are suspected

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 16, 2026
ai-agentsgosqlnodeexpresstestingdatabaseperformance

Works with

  • cursor

Security analysis

A100/100

Scanned October 16, 2026

npx -y skills add RaNDoM6913/claude-code-superkit --skill postgresql-optimization --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Postgresql Optimization?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Postgresql Optimization
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/random6913-postgresql-optimization-claude-code-superkit/badge)](https://www.skillsdirectory.com/skills/random6913-postgresql-optimization-claude-code-superkit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: postgresql-optimization
description: PostgreSQL optimization — query tuning, indexing, EXPLAIN analysis, configuration, maintenance, and monitoring. Use when slow queries, missing indexes, vacuum lag, or performance bottlenecks are suspected
tokens: 1994
user-invocable: false
---

# PostgreSQL Optimization

Diagnose and fix PostgreSQL performance: query tuning, indexing, EXPLAIN analysis, server configuration, maintenance, and monitoring. The core discipline is measure-before-and-after — one change per cycle, validated with `EXPLAIN ANALYZE`.

## Use this skill when

- Optimizing slow PostgreSQL queries
- Designing or reviewing indexing strategies
- Analyzing performance and identifying bottlenecks
- Tuning server configuration
- Managing production maintenance (VACUUM, ANALYZE, bloat)
- Setting up monitoring and alerting

## Do not use this skill when

- You need schema / data-model design — that is out of scope here; this skill tunes an existing schema. For Drizzle ORM schema and migration work, use the `drizzle-orm-expert` skill.
- The bottleneck is at the application layer, not the database.
- You need a database-agnostic optimization guide — this is PostgreSQL-specific.

## Hard Rules

- Measure with `EXPLAIN ANALYZE` BEFORE and AFTER every change — never tune from a guess.
- Change ONE layer at a time (index / query / config / maintenance), then re-measure.
- Use `CONCURRENTLY` for every `CREATE INDEX` and `REINDEX` on live tables — the plain form locks writes.
- NEVER disable autovacuum in production.
- NEVER run `VACUUM FULL` during business hours — it takes an ACCESS EXCLUSIVE lock on the whole table.
- Test config changes on staging first; treat each as a hypothesis to confirm with monitoring.

## Steps

Follow in order. Change one thing, re-measure, then move on.

1. **Identify** — Pin the specific slow query and its bottleneck category. Note the PostgreSQL version and key config (`shared_buffers`, `work_mem`, `effective_cache_size`, `autovacuum`); find the worst queries via `pg_stat_statements` or the slow-query log; check CPU / memory / disk I/O; classify the bottleneck as scan type, join strategy, I/O, locking, or vacuum lag. **Done when:** you have one target query and a suspected category.
2. **Measure the baseline** — Run `EXPLAIN (ANALYZE, BUFFERS)` on the target query before any change. Read the node types; compare estimated vs actual rows (large mismatch = stale statistics → run `ANALYZE`); locate the highest-cost node, highest actual-row node, or largest loop. Use the EXPLAIN Output Interpretation table below. **Done when:** you have a baseline plan and the specific expensive node.
3. **Apply one targeted fix** — Change exactly ONE layer, chosen from the bottleneck: missing index → Indexing Strategy; bad plan or query shape → Query Optimization; memory / checkpoint limits → Configuration Tuning; bloat or stale stats → Maintenance. Validate autovacuum is running before recommending a manual VACUUM; check for stale statistics before rewriting a query. **Done when:** exactly one change is applied.
4. **Verify** — Re-run `EXPLAIN ANALYZE`. Confirm the plan changed as intended and timing dropped. If it did not improve, revert the change and return to Step 2 with a different hypothesis. **Done when:** the second plan shows the expected improvement, or the change is reverted.
5. **Monitor** — Confirm the fix holds under real load using `pg_stat_statements`, `pg_stat_user_tables`, and `pg_stat_user_indexes` over time. Treat every config change as a hypothesis until monitoring confirms it. **Done when:** metrics are stable under production load.

## EXPLAIN Output Interpretation

```sql
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT ...;
```

Node types to know: `Seq Scan`, `Index Scan`, `Index Only Scan`, `Hash Join`, `Nested Loop`, `Merge Join`. A `Seq Scan` on a small table is fine; on a large table it signals a missing or unused index.

| Output sign | Meaning |
|-------------|---------|
| Seq Scan on large table | Missing or unused index |
| Index Scan but slow | Index not selective; high row count returned |
| Nested Loop with large outer | Consider Hash Join (`enable_nestloop=off` for testing) |
| Hash Join with disk batches | `work_mem` too low |
| Sort with `external merge Disk` | `work_mem` too low for this sort |
| Estimated 1, actual 1M | Stale statistics → `ANALYZE table` |
| Estimated 1M, actual 1 | Histogram bucket too small → increase `ALTER TABLE ... ALTER COLUMN ... SET STATISTICS 1000` |

## Indexing Strategy

| Pattern | Index type |
|---------|-----------|
| Equality + range on 1 column | B-tree |
| Multi-column equality | Composite B-tree, most selective first |
| JSONB containment | GIN |
| Full-text search | GIN on `tsvector` |
| Range overlap, scheduling | GiST on range type |
| Hot subset (`WHERE status='active'`) | Partial index |
| Avoid heap reads for extra columns | Covering index with `INCLUDE` |
| Time-series, naturally ordered | BRIN |

Review `pg_stat_user_indexes` — unused indexes waste write performance and bloat.

```sql
-- Always CONCURRENTLY in production:
CREATE INDEX CONCURRENTLY idx_users_email_active
  ON users (email) WHERE active = true;

-- Covering index for index-only scans:
CREATE INDEX CONCURRENTLY idx_orders_user_created
  ON orders (user_id, created_at) INCLUDE (status, total);
```

## Query Optimization

- Rewrite subqueries as JOINs where the planner struggles
- Use CTEs (`WITH ...`) for readability; add `AS MATERIALIZED` when you need an optimization fence — PostgreSQL 12+ inlines plain CTEs
- Cursor-based pagination for large feeds (offset is O(n))
- Use `LIMIT` in subqueries to bound work
- Avoid functions on indexed columns in `WHERE` — prevents index use
- Avoid implicit type casts (`WHERE id = '123'` when id is integer)

## Configuration Tuning

| Setting | Default | Recommendation |
|---------|---------|----------------|
| `shared_buffers` | 128MB | 25% of RAM (dedicated DB server) |
| `work_mem` | 4MB | 16-64MB per connection; multiply by max_connections for peak |
| `effective_cache_size` | 4GB | 50-75% of RAM |
| `checkpoint_completion_target` | 0.9 | Keep 0.9 to spread checkpoint I/O |
| `max_wal_size` | 1GB | Increase if checkpoint frequency too high |
| `autovacuum_vacuum_scale_factor` | 0.2 | 0.01-0.05 for large tables |

## Maintenance

- Regular `VACUUM` reclaims dead tuples from updates/deletes
- `VACUUM ANALYZE` after large data changes updates planner stats
- Monitor bloat: `pg_stat_user_tables`, `pg_total_relation_size`, `pgstattuple`
- Check `pg_stat_user_tables.last_autovacuum`, `n_dead_tup`
- `REINDEX CONCURRENTLY` for bloated indexes (no table lock)

## Partitioning (very large tables)

- Declarative partitioning `PARTITION BY RANGE | LIST | HASH` splits a huge table so the planner prunes irrelevant partitions.
- Range-partition time-series data by date and pair each partition with a BRIN index; drop old partitions instead of `DELETE` to avoid bloat.

## Monitoring

| Tool | Purpose |
|------|---------|
| `pg_stat_statements` | Most time-consuming queries |
| `pg_stat_user_tables` | Table access patterns, dead tuples, vacuum timing |
| `pg_stat_user_indexes` | Index usage; identify unused indexes |
| `pg_locks` | Lock contention slowing queries |
| `pg_stat_activity` | Currently running queries, idle-in-transaction sessions |
| `postgres_exporter` + Grafana | Ongoing visibility |

## Common Anti-Patterns

- `WHERE LOWER(email) = ?` without expression index
- Implicit type cast: `WHERE id = '123'` when id is integer
- `SELECT *` on wide tables (prevents index-only scans)
- `OFFSET 1000000 LIMIT 10` — scans all preceding rows
- `IN (...)` with hundreds of values — consider `ANY(ARRAY[...])` or temp table
- Unnecessary `ORDER BY` in subqueries
- Correlated subqueries executing once per outer row

## Recap — non-negotiables

- Measure with `EXPLAIN ANALYZE` before and after; change one layer per cycle.
- Use `CONCURRENTLY` for every `CREATE INDEX` / `REINDEX` on live tables.
- Never disable autovacuum; never `VACUUM FULL` during business hours.
- Test config on staging first, then confirm with monitoring.

Adapted from VKirill/codex-starter-kit (personal skill, MIT).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.