| name | PostgreSQL Performance Optimization |
| skill_id | postgresql-optimization |
| version | 1.0.0 |
| description | Production-grade PostgreSQL query optimization, indexing strategies, performance tuning, and modern features including pgvector for AI/ML workloads. Master EXPLAIN plans, query analysis, and database design for high-performance applications |
| category | Database & Data |
| tags | ["postgresql","database","performance","optimization","sql","indexing","query-tuning","pgvector","ai-database","explain-plans"] |
| author | mcp-skillset |
| license | MIT |
| created | "2025-11-25T00:00:00.000Z" |
| last_updated | "2025-11-25T00:00:00.000Z" |
| toolchain | ["PostgreSQL 15+","pgvector","pg_stat_statements","EXPLAIN ANALYZE"] |
| frameworks | ["PostgreSQL","SQLAlchemy","asyncpg"] |
| related_skills | ["fastapi-web-development","systematic-debugging","observability-monitoring"] |
PostgreSQL Performance Optimization
Overview
Master PostgreSQL performance optimization with modern techniques for query tuning, indexing, and AI/ML workloads. With 55% of Postgres developers adopting AI tools in 2024, understanding pgvector and performance optimization is critical for building scalable data-intensive applications.
When to Use This Skill
- Slow queries requiring optimization (>100ms response time)
- Designing database schemas for high-performance applications
- Implementing vector similarity search for AI/ML features
- Scaling PostgreSQL for high-concurrency workloads
- Migrating from NoSQL to PostgreSQL for better consistency
- Optimizing ORMs (SQLAlchemy, Django ORM) for production
Core Principles
1. EXPLAIN ANALYZE is Your Best Friend
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, COSTS, TIMING)
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > NOW() - INTERVAL '30 days'
GROUP BY u.id, u.name
ORDER BY order_count DESC
LIMIT 100;
Key Metrics:
- Planning Time: How long query planner took (<10ms ideal)
- Execution Time: Actual query runtime (<100ms for OLTP)
- Rows: Estimated vs actual (mismatches indicate stale statistics)
- Buffers: Shared hits (good), reads (disk I/O, slow)
2. Indexing Strategy
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);
CREATE INDEX idx_active_users ON users(email)
WHERE is_active = true AND deleted_at IS NULL;
CREATE INDEX idx_orders_covering ON orders(user_id, status)
INCLUDE (created_at, total_amount);
CREATE INDEX idx_products_search ON products
USING GIN (to_tsvector('english', name || ' ' || description));
CREATE INDEX idx_tags_gin ON posts USING GIN(tags);
CREATE INDEX idx_locations_gist ON stores
USING GIST (location);
CREATE INDEX idx_logs_created ON logs USING BRIN(created_at);
Index Selection Rules:
- Equality filters (
WHERE id = 123) → B-Tree
- Range queries (
WHERE created_at > NOW() - '7 days') → B-Tree
- Full-text search → GIN with
tsvector
- JSONB queries → GIN
- Time-series/append-only → BRIN (90% smaller than B-Tree)
- Vector similarity (
ORDER BY embedding <=> query_vector) → HNSW (pgvector)
3. Query Optimization Patterns
SELECT * FROM users WHERE email = 'user@example.com';
SELECT id, name, email FROM users WHERE email = 'user@example.com';
FOR user IN (SELECT id FROM users):
SELECT * FROM orders WHERE user_id = user.id;
SELECT u.name, o.id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
ALTER TABLE users ALTER COLUMN email TYPE CITEXT;
SELECT * FROM users WHERE email = 'USER@EXAMPLE.COM';
SELECT * FROM products WHERE category = 'electronics' OR category = 'books';
SELECT * FROM products WHERE category IN ('electronics', 'books');
SELECT * FROM products WHERE category = 'electronics'
UNION ALL
SELECT * FROM products WHERE category = 'books';
4. Connection Pooling
import psycopg2
def get_user(user_id):
conn = psycopg2.connect("postgresql://localhost/db")
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
result = cursor.fetchone()
conn.close()
return result
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
engine = create_async_engine(
"postgresql+asyncpg://localhost/db",
pool_size=20,
max_overflow=0,
pool_pre_ping=True,
echo_pool=True,
)
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
Connection Pool Settings:
pool_size: Core connections (20-50 for web apps)
max_overflow: Extra connections (0 to prevent overload)
pool_recycle: Close connections after N seconds (3600 = 1 hour)
5. pgvector for AI/ML Workloads
CREATE EXTENSION vector;
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536)
);
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
SELECT id, content, 1 - (embedding <=> '[0.1, 0.2, ...]'::vector) AS similarity
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
pgvector Best Practices:
- Use HNSW index for production (10-100x faster than IVFFlat)
- Normalize embeddings before storage for cosine similarity
- Tune
m (16-48) and ef_construction (64-200) for accuracy/speed tradeoff
- Use
SET ivfflat.probes = 10 for IVFFlat search quality
Performance Tuning Checklist
PostgreSQL Configuration (postgresql.conf)
shared_buffers = 4GB
effective_cache_size = 12GB
work_mem = 64MB
maintenance_work_mem = 1GB
random_page_cost = 1.1
effective_io_concurrency = 200
wal_buffers = 16MB
max_wal_size = 4GB
min_wal_size = 1GB
checkpoint_completion_target = 0.9
max_connections = 200
log_min_duration_statement = 1000
log_line_prefix = '%m [%p] %u@%d '
log_statement = 'none'
Essential Extensions
CREATE EXTENSION pg_stat_statements;
SELECT query, calls, total_exec_time, mean_exec_time, max_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
SELECT * FROM products
WHERE to_tsvector('english', name) @@ to_tsquery('laptop & portable');
Common Anti-Patterns
❌ DON'T: Use OFFSET for pagination
SELECT * FROM posts
ORDER BY created_at DESC
LIMIT 50 OFFSET 100000;
SELECT * FROM posts
WHERE created_at < '2024-01-01 12:00:00'
ORDER BY created_at DESC
LIMIT 50;
❌ DON'T: Run VACUUM FULL in production
VACUUM FULL users;
VACUUM ANALYZE users;
❌ DON'T: Ignore table statistics
ANALYZE users;
SELECT schemaname, tablename, last_analyze, last_autoanalyze
FROM pg_stat_user_tables
WHERE schemaname = 'public';
Testing & Monitoring
SELECT
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename) -
pg_relation_size(schemaname||'.'||tablename)) AS bloat
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;
SELECT
schemaname || '.' || tablename AS table,
indexname AS index,
idx_scan as scans,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan ASC, pg_relation_size(indexrelid) DESC;
SELECT
schemaname,
tablename,
seq_scan,
seq_tup_read,
idx_scan,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_stat_user_tables
WHERE seq_scan > 0 AND idx_scan = 0
AND pg_total_relation_size(schemaname||'.'||tablename) > 1000000
ORDER BY seq_tup_read DESC;
Related Skills
- fastapi-web-development: Optimize FastAPI + PostgreSQL integration
- observability-monitoring: Monitor PostgreSQL with Prometheus
- systematic-debugging: Debug query performance issues
Additional Resources
Example Questions
- "Why is this query slow? How do I read the EXPLAIN plan?"
- "What type of index should I use for full-text search?"
- "How do I implement vector similarity search with pgvector?"
- "Show me how to find unused indexes in my database"
- "What's the best way to paginate large result sets?"
- "How do I configure connection pooling for a high-traffic API?"