
Postgres Performance
- 1 installs
- 404 repo stars
- Updated August 5, 2026
- aiskillstore/marketplace
postgres-performance is a Claude Code skill providing PostgreSQL patterns for query optimization, scaling, and debugging performance issues.
About
postgres-performance is a Claude Code skill with PostgreSQL patterns for optimizing queries, designing for scale, and debugging performance issues. It walks through identifying slow queries with pg_stat_statements, reading EXPLAIN plans, and fixing them with the right indexes. A developer uses it to apply covering indexes, keyset pagination, batched updates, and efficient aggregations when a database is slow at scale.
- Query optimization workflow: find slow queries, read EXPLAIN plans, add indexes, verify
- Patterns for covering indexes, keyset pagination, batch processing, and efficient aggregations
- SQL plus SQLAlchemy/Python examples for scaling PostgreSQL interactions
Postgres Performance by the numbers
- 1 all-time installs (skills.sh)
- Ranked #765 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
postgres-performance capabilities & compatibility
Free; a knowledge/pattern skill with no external service
- Capabilities
- database · query optimization · performance tuning
- Works with
- postgres
- Use cases
- database · debugging
- Pricing
- Free
What postgres-performance says it does
High-performance PostgreSQL patterns. Use when optimizing queries, designing for scale, or debugging performance issues.
A query that takes 50ms at 1K rows takes 5s at 100K rows.
npx skills add https://github.com/aiskillstore/marketplace --skill postgres-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 404 |
| Last updated | August 5, 2026 |
| Repository | aiskillstore/marketplace ↗ |
What it does
Optimize slow PostgreSQL queries and design database access patterns that scale to large tables.
Who is it for?
Fixing slow PostgreSQL queries and designing indexes and pagination for scale
Skip if: Non-Postgres databases or ORM/schema design unrelated to performance
When should I use this skill?
Optimizing queries, designing for scale, or debugging PostgreSQL performance issues
What you get
Queries that scale via correct indexes, keyset pagination, and batched writes.
- optimized indexes
- faster query plans
- keyset pagination and batch-update patterns
By the numbers
- lists 5 EXPLAIN plan warning signs
- batch processing example uses 10000-row batches
Files
PostgreSQL Performance Engineering
Problem Statement
Performance problems compound. A query that takes 50ms at 1K rows takes 5s at 100K rows. This skill covers patterns for building performant database interactions from the start and fixing performance issues.
---
Pattern: Query Optimization Workflow
Step 1: Identify Slow Queries
-- Enable pg_stat_statements (if not already)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Find slowest queries
SELECT
query,
calls,
round(mean_exec_time::numeric, 2) as avg_ms,
round(total_exec_time::numeric, 2) as total_ms,
rows
FROM pg_stat_statements
WHERE calls > 10
ORDER BY mean_exec_time DESC
LIMIT 20;Step 2: Analyze Query Plan
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM assessments
WHERE user_id = 'abc-123'
ORDER BY created_at DESC
LIMIT 10;What to look for:
| Warning Sign | Problem | Solution |
|---|---|---|
| Seq Scan on large table | Missing index | Add index |
High loops count | N+1 in join | Rewrite query, add index |
| Sort with high cost | No index for ORDER BY | Covering index |
| Hash/Merge Join with high rows | Large intermediate result | Filter earlier, better indexes |
| Buffers: shared read high | Data not cached | More RAM, or query less data |
Step 3: Fix and Verify
-- Add index
CREATE INDEX CONCURRENTLY ix_assessments_user_created
ON assessments (user_id, created_at DESC);
-- Verify improvement
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM assessments
WHERE user_id = 'abc-123'
ORDER BY created_at DESC
LIMIT 10;
-- Should now show "Index Scan" instead of "Seq Scan"---
Pattern: Covering Indexes (Index-Only Scans)
Problem: Query reads index, then fetches rows from table (heap fetch).
-- Query
SELECT id, title, status FROM assessments WHERE user_id = ?;
-- Regular index: requires heap fetch
CREATE INDEX ix_assessments_user ON assessments (user_id);
-- Plan: Index Scan + Heap Fetches
-- ✅ Covering index: all columns in index
CREATE INDEX ix_assessments_user_covering
ON assessments (user_id)
INCLUDE (id, title, status);
-- Plan: Index Only Scan (no heap fetch, much faster)When to use:
- Frequently run queries
- Queries selecting few columns
- Tables with many columns (heap fetch is expensive)
---
Pattern: Pagination at Scale
-- ❌ SLOW: OFFSET-based pagination
SELECT * FROM events ORDER BY created_at DESC LIMIT 20 OFFSET 10000;
-- Must scan and discard 10,000 rows!
-- ✅ FAST: Cursor-based (keyset) pagination
SELECT * FROM events
WHERE created_at < '2024-01-15T10:30:00Z' -- Last seen timestamp
ORDER BY created_at DESC
LIMIT 20;
-- Jumps directly to the right place via index
-- For compound cursor (when duplicates possible):
SELECT * FROM events
WHERE (created_at, id) < ('2024-01-15T10:30:00Z', 'last-id')
ORDER BY created_at DESC, id DESC
LIMIT 20;In SQLAlchemy:
# Cursor-based pagination
async def get_events_page(
session: AsyncSession,
cursor_time: datetime | None,
cursor_id: UUID | None,
limit: int = 20,
) -> list[Event]:
query = select(Event).order_by(Event.created_at.desc(), Event.id.desc())
if cursor_time and cursor_id:
query = query.where(
tuple_(Event.created_at, Event.id) < (cursor_time, cursor_id)
)
result = await session.execute(query.limit(limit))
return result.scalars().all()---
Pattern: Batch Processing
-- ❌ SLOW: One huge query/update
UPDATE events SET processed = true WHERE processed = false;
-- Locks millions of rows, times out
-- ✅ FAST: Batch processing
DO $$
DECLARE
batch_size INT := 10000;
rows_affected INT;
BEGIN
LOOP
UPDATE events
SET processed = true
WHERE id IN (
SELECT id FROM events
WHERE processed = false
LIMIT batch_size
FOR UPDATE SKIP LOCKED
);
GET DIAGNOSTICS rows_affected = ROW_COUNT;
IF rows_affected = 0 THEN
EXIT;
END IF;
COMMIT;
PERFORM pg_sleep(0.1); -- Brief pause to let other queries through
END LOOP;
END $$;In Python:
async def process_in_batches(session: AsyncSession, batch_size: int = 10000):
while True:
result = await session.execute(
text("""
UPDATE events SET processed = true
WHERE id IN (
SELECT id FROM events
WHERE processed = false
LIMIT :batch_size
FOR UPDATE SKIP LOCKED
)
RETURNING id
"""),
{"batch_size": batch_size}
)
updated = result.fetchall()
await session.commit()
if len(updated) == 0:
break
await asyncio.sleep(0.1)---
Pattern: Efficient Aggregations
-- ❌ SLOW: Count with complex WHERE
SELECT COUNT(*) FROM events WHERE user_id = ? AND status = 'active';
-- Scans all matching rows
-- ✅ FAST: Approximate count (for large tables)
SELECT reltuples::bigint AS estimate
FROM pg_class
WHERE relname = 'events';
-- ✅ FAST: Maintain counter cache
-- Add column: assessments.answer_count
-- Update on INSERT/DELETE to answers
-- ✅ FAST: Materialized view for complex aggregations
CREATE MATERIALIZED VIEW user_stats AS
SELECT
user_id,
COUNT(*) as total_assessments,
AVG(rating) as avg_rating
FROM assessments
GROUP BY user_id;
-- Refresh periodically
REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats;---
Pattern: Connection Pool Tuning
# Async SQLAlchemy with proper pool settings
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.pool import NullPool, AsyncAdaptedQueuePool
# For serverless/Lambda (no persistent connections)
engine = create_async_engine(
DATABASE_URL,
poolclass=NullPool, # New connection per request
)
# For long-running servers
engine = create_async_engine(
DATABASE_URL,
poolclass=AsyncAdaptedQueuePool,
pool_size=10, # Base connections
max_overflow=20, # Extra connections under load
pool_timeout=30, # Wait for connection
pool_recycle=1800, # Recycle connections every 30 min
pool_pre_ping=True, # Test connection before use
)PostgreSQL side:
-- Check max connections
SHOW max_connections; -- Default 100
-- See current connections
SELECT count(*) FROM pg_stat_activity;
-- Connection per application
SELECT application_name, count(*)
FROM pg_stat_activity
GROUP BY application_name;---
Pattern: Read Replicas
# Route reads to replica, writes to primary
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
primary_engine = create_async_engine(PRIMARY_URL)
replica_engine = create_async_engine(REPLICA_URL)
class RoutingSession(Session):
def get_bind(self, mapper=None, clause=None):
if self._flushing or self.is_modified():
return primary_engine.sync_engine
return replica_engine.sync_engine---
Pattern: Denormalization for Read Performance
-- ❌ SLOW: Joining 4 tables for common query
SELECT
a.id, a.title, u.name as user_name,
COUNT(q.id) as question_count,
AVG(ans.value) as avg_score
FROM assessments a
JOIN users u ON a.user_id = u.id
JOIN questions q ON q.assessment_id = a.id
LEFT JOIN answers ans ON ans.question_id = q.id
GROUP BY a.id, a.title, u.name;
-- ✅ FAST: Denormalized columns
ALTER TABLE assessments ADD COLUMN user_name VARCHAR(100);
ALTER TABLE assessments ADD COLUMN question_count INT DEFAULT 0;
ALTER TABLE assessments ADD COLUMN avg_score NUMERIC(3,2);
-- Update via triggers or application code
-- Query becomes simple:
SELECT id, title, user_name, question_count, avg_score FROM assessments;Tradeoffs:
- ✅ Much faster reads
- ❌ More complex writes (must update denormalized data)
- ❌ Potential for stale data
---
Pattern: Partitioning Large Tables
-- Partition events by month
CREATE TABLE events (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
event_type VARCHAR(50),
created_at TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (created_at);
-- Create partitions
CREATE TABLE events_2024_01 PARTITION OF events
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE events_2024_02 PARTITION OF events
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
-- Query specific partition (fast)
SELECT * FROM events WHERE created_at >= '2024-01-15' AND created_at < '2024-02-01';
-- Drop old data instantly
DROP TABLE events_2023_01; -- Much faster than DELETE---
Pattern: Caching Strategy
# Cache frequently-read, rarely-changed data
import redis.asyncio as redis
import json
cache = redis.from_url(REDIS_URL)
async def get_user_stats(user_id: UUID) -> UserStats:
cache_key = f"user_stats:{user_id}"
# Try cache first
cached = await cache.get(cache_key)
if cached:
return UserStats.model_validate_json(cached)
# Query database
async with get_session() as session:
stats = await calculate_user_stats(session, user_id)
# Cache for 5 minutes
await cache.setex(cache_key, 300, stats.model_dump_json())
return stats
# Invalidate on write
async def update_user_assessment(user_id: UUID, ...):
# ... update database ...
await cache.delete(f"user_stats:{user_id}")---
Performance Monitoring Queries
-- Table bloat (needs VACUUM)
SELECT
schemaname, relname,
n_dead_tup as dead_tuples,
n_live_tup as live_tuples,
round(n_dead_tup::numeric / NULLIF(n_live_tup, 0) * 100, 2) as dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;
-- Index bloat
SELECT
indexrelname as index,
pg_size_pretty(pg_relation_size(indexrelid)) as size,
idx_scan as scans
FROM pg_stat_user_indexes
WHERE idx_scan = 0 -- Unused indexes
ORDER BY pg_relation_size(indexrelid) DESC;
-- Cache hit ratio (should be > 99%)
SELECT
sum(blks_hit) * 100.0 / sum(blks_hit + blks_read) as cache_hit_ratio
FROM pg_stat_database;
-- Long-running queries
SELECT
pid,
now() - query_start as duration,
query
FROM pg_stat_activity
WHERE state = 'active'
AND query NOT LIKE '%pg_stat%'
AND now() - query_start > interval '30 seconds';---
Performance Checklist
Before deploying:
- [ ] Slow queries identified and optimized
- [ ] Indexes match query patterns
- [ ] Covering indexes for frequent queries
- [ ] Pagination uses cursor-based (not OFFSET)
- [ ] Large tables partitioned if > 10M rows
- [ ] Connection pool sized appropriately
- [ ] Cache layer for hot data
- [ ] Monitoring in place for slow queries
- [ ] VACUUM and ANALYZE scheduled
{
"schema_version": "2.0",
"meta": {
"generated_at": "2026-01-21T16:47:13.097Z",
"slug": "cjharmath-postgres-performance",
"source_url": "https://github.com/CJHarmath/claude-agents-skills/tree/main/skills/postgres-performance",
"source_ref": "main",
"model": "claude",
"analysis_version": "3.0.0",
"source_type": "community",
"content_hash": "a9d3b1a51f621c8423720a8a95bda5c1ded938920d247baee3b504183e0ab608",
"tree_hash": "3609eb55c543d16994aa17cb143da7d0b9943c80f89ffc87b00b1e9361370465"
},
"skill": {
"name": "postgres-performance",
"description": "High-performance PostgreSQL patterns. Use when optimizing queries, designing for scale, or debugging performance issues.",
"summary": "High-performance PostgreSQL patterns for query optimization, indexing strategies, and database scaling.",
"icon": "📦",
"version": "1.0.0",
"author": "CJHarmath",
"license": "MIT",
"tags": [
"postgresql",
"database-optimization",
"query-performance",
"sql",
"database-engineering"
],
"supported_tools": [
"claude",
"codex",
"claude-code"
],
"risk_factors": []
},
"security_audit": {
"risk_level": "low",
"is_blocked": false,
"safe_to_publish": true,
"summary": "Static analysis flagged 78 potential issues but all are false positives. The scanner misidentified SQL keywords as cryptographic code and markdown code fences as shell execution. This is a legitimate PostgreSQL performance optimization skill containing documentation and example queries. Risk factors are standard for documentation skills that include code examples.",
"risk_factor_evidence": [],
"critical_findings": [],
"high_findings": [],
"medium_findings": [],
"low_findings": [],
"dangerous_patterns": [],
"files_scanned": 2,
"total_lines": 1166,
"audit_model": "claude",
"audited_at": "2026-01-21T16:47:13.097Z"
},
"content": {
"user_title": "Optimize PostgreSQL Query Performance",
"value_statement": "Database performance issues slow down applications and frustrate users. This skill provides proven patterns for PostgreSQL optimization including indexing strategies, query tuning, and scalable architecture patterns.",
"seo_keywords": [
"PostgreSQL performance optimization",
"PostgreSQL query tuning",
"database indexing",
"SQL performance",
"PostgreSQL scaling",
"query optimization",
"database performance",
"Claude",
"Codex",
"Claude Code"
],
"actual_capabilities": [
"Analyze slow queries using pg_stat_statements and EXPLAIN",
"Design effective indexes including covering indexes for index-only scans",
"Implement cursor-based pagination for large datasets",
"Optimize batch processing with FOR UPDATE SKIP LOCKED",
"Configure connection pooling for serverless and long-running applications",
"Set up read replicas for query load distribution"
],
"limitations": [
"Does not execute queries or modify database schema",
"Cannot access actual database statistics without connection",
"Cannot replace proper database monitoring and observability tools",
"Patterns are general guidance, not specific to every workload"
],
"use_cases": [
{
"title": "Debug Slow Application Queries",
"description": "Identify and fix performance bottlenecks in application queries using PostgreSQL diagnostic tools and EXPLAIN analysis.",
"target_user": "Backend developers troubleshooting production performance issues"
},
{
"title": "Design Scalable Database Schema",
"description": "Apply indexing strategies, partitioning, and denormalization patterns for high-throughput database workloads.",
"target_user": "Database engineers designing new systems or refactoring existing schemas"
},
{
"title": "Optimize Batch Operations",
"description": "Process large datasets efficiently without locking or causing timeouts using batch patterns with SKIP LOCKED.",
"target_user": "Data engineers building ETL pipelines and data processing jobs"
}
],
"prompt_templates": [
{
"title": "Quick Query Analysis",
"prompt": "My PostgreSQL query is slow. Analyze and optimize it:\n\n```sql\nSELECT * FROM orders WHERE user_id = ? AND status = 'pending' ORDER BY created_at DESC LIMIT 20;\n```\n\nHow can I improve this query?",
"scenario": "Getting started with query optimization"
},
{
"title": "Index Strategy Design",
"prompt": "I need to optimize these frequently-run queries on a table with 10M+ rows:\n\n1. SELECT * FROM products WHERE category_id = ? AND in_stock = true\n2. SELECT id, name, price FROM products WHERE category_id = ? ORDER BY price ASC\n3. SELECT COUNT(*) FROM products WHERE category_id = ?\n\nDesign an optimal index strategy.",
"scenario": "Designing indexes for specific query patterns"
},
{
"title": "Pagination Migration",
"prompt": "Our API uses OFFSET pagination and it is slow for deep pages:\n\n```python\n# Current implementation\ndef get_events(page: int, limit: int = 20):\n offset = (page - 1) * limit\n return db.execute(\"SELECT * FROM events ORDER BY created_at DESC LIMIT ? OFFSET ?\", limit, offset)\n```\n\nConvert this to cursor-based pagination with example code.",
"scenario": "Migrating from OFFSET to cursor pagination"
},
{
"title": "Connection Pool Configuration",
"prompt": "Configure PostgreSQL connection pooling for our use case:\n\n- Serverless Python API on Lambda (spiky traffic, ~1000 req/min)\n- We see connection errors during traffic spikes\n- Database has 16GB RAM, shared_buffers = 4GB\n\nWhat pool settings do you recommend?",
"scenario": "Optimizing connection handling for specific deployment"
}
],
"output_examples": [
{
"input": "My query SELECT * FROM events WHERE user_id = 'abc-123' ORDER BY created_at DESC LIMIT 10 is slow on a table with 5M rows.",
"output": "The query likely performs a sequential scan on the large events table. Here is the optimization approach:\n\n**Problem:** Missing index for the filter and ORDER BY columns.\n\n**Solution:** Create a covering index:\n```sql\nCREATE INDEX CONCURRENTLY ix_events_user_created \nON events (user_id, created_at DESC);\n```\n\nThis index allows:\n1. Filtering by user_id directly from the index\n2. Sorting by created_at DESC using the index order\n3. Index-only scan if you select only indexed columns\n\n**Verification:**\n```sql\nEXPLAIN (ANALYZE, BUFFERS)\nSELECT * FROM events \nWHERE user_id = 'abc-123' \nORDER BY created_at DESC \nLIMIT 10;\n```\n\nLook for \"Index Scan\" instead of \"Seq Scan\" in the plan."
},
{
"input": "How should I implement pagination for a high-traffic API endpoint?",
"output": "Avoid OFFSET-based pagination for large datasets. Use cursor-based (keyset) pagination instead.\n\n**Why OFFSET is slow:**\n```sql\nSELECT * FROM events ORDER BY created_at DESC LIMIT 20 OFFSET 10000;\n-- Must scan and discard 10,000 rows before returning results\n```\n\n**Cursor-based approach:**\n```sql\nSELECT * FROM events \nWHERE created_at < '2024-01-15T10:30:00Z'\nORDER BY created_at DESC \nLIMIT 20;\n-- Jumps directly to the right place via index\n```\n\n**Python implementation:**\n```python\nasync def get_events_page(session, cursor_time=None, limit=20):\n query = select(Event).order_by(Event.created_at.desc())\n if cursor_time:\n query = query.where(Event.created_at < cursor_time)\n return await session.execute(query.limit(limit))\n```\n\n**Trade-offs:**\n- Cannot jump to arbitrary page numbers\n- Requires passing cursor to client\n- Much faster for deep pagination"
}
],
"best_practices": [
"Always verify query improvements with EXPLAIN (ANALYZE, BUFFERS) before and after changes",
"Use covering indexes (INCLUDE clause) to avoid heap fetches for frequent queries",
"Implement cursor-based pagination instead of OFFSET for any table over 100K rows"
],
"anti_patterns": [
"Using SELECT * in performance-critical queries - specify only needed columns",
"Running large UPDATE or DELETE operations without batching - causes locks and timeouts",
"Skipping the FOR UPDATE SKIP LOCKED pattern in concurrent batch processing"
],
"faq": [
{
"question": "Does this skill execute queries against my database?",
"answer": "No. This skill provides patterns, code examples, and guidance. You must execute any SQL commands yourself after reviewing the recommendations."
},
{
"question": "How do I identify which queries are slow in production?",
"answer": "Enable the pg_stat_statements extension and query it to find your slowest queries by average execution time. The skill includes the exact SQL to use."
},
{
"question": "What is the difference between CREATE INDEX and CREATE INDEX CONCURRENTLY?",
"answer": "CONCURRENTLY creates the index without blocking writes to the table. Use it in production. Regular CREATE INDEX locks the table for writes during index build."
},
{
"question": "When should I use table partitioning?",
"answer": "Partition when tables exceed 10M rows and you have natural partition keys (dates, categories). Partitioning improves query performance and makes deleting old data much faster."
},
{
"question": "How do I choose between read replicas and caching?",
"answer": "Use read replicas to scale query throughput across multiple connections. Use caching for frequently-read, rarely-changed data. Both strategies complement each other."
},
{
"question": "What connection pool settings work best for serverless?",
"answer": "For serverless/Lambda with no persistent connections, use NullPool (new connection per request). For long-running services, use AsyncAdaptedQueuePool with appropriate pool_size and max_overflow values."
}
]
},
"file_structure": [
{
"name": "SKILL.md",
"type": "file",
"path": "SKILL.md",
"lines": 441
}
]
}
Related skills
FAQ
How do I find slow queries?
Enable pg_stat_statements and order by mean_exec_time to surface the slowest queries.
What replaces OFFSET pagination?
Cursor-based (keyset) pagination that filters on the last-seen key and jumps directly via the index.