PostgreSQL Performance Tuning: Indexes, Queries, and EXPLAIN ANALYZE

A hands-on guide to diagnosing and fixing slow PostgreSQL queries using EXPLAIN ANALYZE and proper indexing.

Start With EXPLAIN ANALYZE

Guessing at performance problems wastes time. Always start by looking at the actual query plan:

EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 4821 AND status = 'shipped';

Look for Seq Scan on large tables — that’s usually your first sign a missing index is causing a full table scan.

Adding the Right Index

CREATE INDEX idx_orders_customer_status
ON orders (customer_id, status);

Column order matters: put the most selective, most frequently filtered column first when queries commonly filter on both.

Composite vs Single-Column Indexes

A composite index on (customer_id, status) serves queries filtering on customer_id alone or on both columns together, but won’t help a query filtering on status alone. Match your indexes to your actual query patterns, not every possible combination.

Avoiding N+1 Queries

-- N+1: one query per order to fetch items (bad)
SELECT * FROM order_items WHERE order_id = 1;
SELECT * FROM order_items WHERE order_id = 2;
-- ... repeated for every order

-- Fixed: single query with IN
SELECT * FROM order_items WHERE order_id IN (1, 2, 3, 4, 5);

Using Partial Indexes

If you frequently query only active or recent rows, a partial index keeps the index smaller and faster:

CREATE INDEX idx_orders_pending
ON orders (created_at)
WHERE status = 'pending';

Watch for Implicit Type Casts

-- Prevents index usage if customer_id is an integer column
SELECT * FROM orders WHERE customer_id = '4821';

-- Correct type, index-friendly
SELECT * FROM orders WHERE customer_id = 4821;

Connection Pooling

Beyond query-level tuning, use a connection pooler like PgBouncer for high-concurrency applications — PostgreSQL’s per-connection memory overhead makes thousands of direct connections expensive.

Monitoring Ongoing Performance

SELECT query, calls, mean_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

The pg_stat_statements extension surfaces your slowest queries in aggregate, which is far more useful than debugging one-off slow requests reactively.

Conclusion

Most PostgreSQL performance issues come down to missing or mismatched indexes and N+1 query patterns. Make EXPLAIN ANALYZE a habit before assuming you need a bigger database instance.