Back to Blog
PostgreSQLDatabasePerformanceOptimization

PostgreSQL Performance Tuning Guide

Optimize PostgreSQL for production. From configuration to query optimization to monitoring patterns.

B
Bootspring Team
Engineering
August 10, 2022
5 min read

PostgreSQL performance depends on proper configuration and query optimization. Here's how to tune it effectively.

Memory Configuration#

1-- View current settings 2SHOW shared_buffers; 3SHOW effective_cache_size; 4SHOW work_mem; 5 6-- postgresql.conf recommendations 7 8-- Shared buffers: 25% of RAM (up to 8GB) 9shared_buffers = 4GB -- For 16GB RAM server 10 11-- Effective cache size: 75% of RAM 12effective_cache_size = 12GB 13 14-- Work memory per operation (careful with this) 15work_mem = 64MB -- Consider: work_mem * max_connections 16 17-- Maintenance operations 18maintenance_work_mem = 1GB 19 20-- WAL buffers 21wal_buffers = 64MB

Connection Settings#

1-- postgresql.conf 2 3-- Maximum connections 4max_connections = 100 -- Lower than you think 5 6-- Use connection pooling (PgBouncer) for more connections 7 8-- Connection timeout 9tcp_keepalives_idle = 60 10tcp_keepalives_interval = 10 11tcp_keepalives_count = 6

Query Analysis#

1-- Enable query timing 2\timing on 3 4-- Explain query plan 5EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) 6SELECT * FROM orders 7WHERE user_id = 123 8AND created_at > '2024-01-01'; 9 10-- Understanding EXPLAIN output 11/* 12Seq Scan: Full table scan (often bad for large tables) 13Index Scan: Using index (good) 14Index Only Scan: Data from index only (best) 15Bitmap Heap Scan: Multiple index conditions 16Nested Loop: Joining small result sets 17Hash Join: Joining larger result sets 18*/ 19 20-- Find slow queries 21SELECT query, calls, mean_time, total_time 22FROM pg_stat_statements 23ORDER BY total_time DESC 24LIMIT 10;

Index Optimization#

1-- Create appropriate indexes 2CREATE INDEX idx_orders_user_id ON orders(user_id); 3CREATE INDEX idx_orders_created_at ON orders(created_at); 4 5-- Composite index for common queries 6CREATE INDEX idx_orders_user_created 7ON orders(user_id, created_at DESC); 8 9-- Partial index for common filters 10CREATE INDEX idx_orders_pending 11ON orders(created_at) 12WHERE status = 'pending'; 13 14-- Index for pattern matching 15CREATE INDEX idx_users_email_pattern 16ON users(email varchar_pattern_ops); 17 18-- Check index usage 19SELECT 20 schemaname, 21 tablename, 22 indexname, 23 idx_scan, 24 idx_tup_read 25FROM pg_stat_user_indexes 26WHERE idx_scan = 0 27ORDER BY pg_relation_size(indexrelid) DESC; 28 29-- Find missing indexes 30SELECT 31 relname, 32 seq_scan, 33 seq_tup_read, 34 idx_scan, 35 idx_tup_fetch 36FROM pg_stat_user_tables 37WHERE seq_scan > idx_scan 38AND pg_relation_size(relid) > 10000000 39ORDER BY seq_tup_read DESC;

Table Maintenance#

1-- Analyze table statistics 2ANALYZE orders; 3 4-- Vacuum to reclaim space 5VACUUM orders; 6 7-- Full vacuum (locks table) 8VACUUM FULL orders; 9 10-- Reindex 11REINDEX INDEX idx_orders_user_id; 12 13-- Automatic maintenance settings 14-- postgresql.conf 15autovacuum = on 16autovacuum_vacuum_scale_factor = 0.1 17autovacuum_analyze_scale_factor = 0.05 18 19-- Check table bloat 20SELECT 21 schemaname, 22 tablename, 23 n_dead_tup, 24 n_live_tup, 25 round(n_dead_tup * 100.0 / nullif(n_live_tup, 0), 2) as dead_ratio 26FROM pg_stat_user_tables 27WHERE n_dead_tup > 1000 28ORDER BY n_dead_tup DESC;

Query Optimization#

1-- Use EXISTS instead of IN for subqueries 2-- ❌ Slow 3SELECT * FROM users 4WHERE id IN (SELECT user_id FROM orders WHERE total > 100); 5 6-- ✓ Faster 7SELECT * FROM users u 8WHERE EXISTS ( 9 SELECT 1 FROM orders o 10 WHERE o.user_id = u.id AND o.total > 100 11); 12 13-- Use LIMIT with ORDER BY 14-- ❌ Fetches all rows first 15SELECT * FROM orders ORDER BY created_at DESC; 16 17-- ✓ Efficient with index 18SELECT * FROM orders 19ORDER BY created_at DESC 20LIMIT 20; 21 22-- Avoid SELECT * 23-- ❌ Fetches unnecessary columns 24SELECT * FROM users WHERE id = 1; 25 26-- ✓ Select only needed columns 27SELECT id, name, email FROM users WHERE id = 1; 28 29-- Use covering indexes 30CREATE INDEX idx_orders_covering 31ON orders(user_id) 32INCLUDE (total, status); 33 34-- Query uses index-only scan 35SELECT total, status FROM orders WHERE user_id = 123;

Partitioning#

1-- Range partitioning for time-series data 2CREATE TABLE events ( 3 id BIGSERIAL, 4 created_at TIMESTAMP NOT NULL, 5 event_type TEXT, 6 data JSONB 7) PARTITION BY RANGE (created_at); 8 9-- Create partitions 10CREATE TABLE events_2024_01 PARTITION OF events 11 FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); 12 13CREATE TABLE events_2024_02 PARTITION OF events 14 FOR VALUES FROM ('2024-02-01') TO ('2024-03-01'); 15 16-- Automatic partition creation (pg_partman extension) 17-- Or use application logic to create partitions 18 19-- Queries automatically use appropriate partition 20SELECT * FROM events 21WHERE created_at >= '2024-01-15' 22AND created_at < '2024-01-20';

Monitoring Queries#

1-- Active connections 2SELECT 3 datname, 4 usename, 5 client_addr, 6 state, 7 query_start, 8 query 9FROM pg_stat_activity 10WHERE state != 'idle' 11ORDER BY query_start; 12 13-- Lock monitoring 14SELECT 15 blocked.pid AS blocked_pid, 16 blocked.query AS blocked_query, 17 blocking.pid AS blocking_pid, 18 blocking.query AS blocking_query 19FROM pg_stat_activity blocked 20JOIN pg_stat_activity blocking 21ON blocking.pid = ANY(pg_blocking_pids(blocked.pid)); 22 23-- Table sizes 24SELECT 25 relname AS table_name, 26 pg_size_pretty(pg_total_relation_size(relid)) AS total_size, 27 pg_size_pretty(pg_relation_size(relid)) AS data_size, 28 pg_size_pretty(pg_indexes_size(relid)) AS index_size 29FROM pg_stat_user_tables 30ORDER BY pg_total_relation_size(relid) DESC 31LIMIT 10; 32 33-- Cache hit ratio (should be > 99%) 34SELECT 35 sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS ratio 36FROM pg_statio_user_tables;

Configuration Checklist#

Memory: ✓ shared_buffers = 25% of RAM ✓ effective_cache_size = 75% of RAM ✓ work_mem based on query complexity ✓ maintenance_work_mem for maintenance WAL: ✓ wal_buffers = 64MB ✓ checkpoint_completion_target = 0.9 ✓ max_wal_size = 4GB Query Planner: ✓ random_page_cost = 1.1 (SSD) ✓ effective_io_concurrency = 200 (SSD) ✓ default_statistics_target = 100 Connections: ✓ Use connection pooling ✓ Appropriate max_connections ✓ Statement timeout set

Best Practices#

Indexes: ✓ Index foreign keys ✓ Use partial indexes ✓ Monitor unused indexes ✓ Consider covering indexes Queries: ✓ Use EXPLAIN ANALYZE ✓ Avoid SELECT * ✓ Use prepared statements ✓ Batch bulk operations Maintenance: ✓ Regular VACUUM ANALYZE ✓ Monitor table bloat ✓ Archive old data ✓ Test configuration changes

Conclusion#

PostgreSQL performance tuning requires understanding your workload. Configure memory based on available resources, create appropriate indexes for your queries, and monitor regularly. Use EXPLAIN ANALYZE to understand query behavior and pg_stat_statements to find optimization targets.

Share this article

Help spread the word about Bootspring