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 = 64MBConnection 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 = 6Query 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.