Table of Contents
- Memory Configuration: The Foundation of Performance
- Shared Buffers
- Work Mem
- Maintenance Work Mem
- Effective Cache Size
- Connection Management: Balancing Throughput and Overhead
- Max Connections
- Connection Pooling
- Query Optimization: Tuning for Faster Execution
- Random Page Cost vs. Sequential Page Cost
- Planner Method Configuration
- WAL Tuning: Accelerating Writes and Crash Recovery
- WAL Buffers
- Checkpoint Settings
- WAL Writer Delay
- Indexing Strategies: Beyond Basic B-Trees
- BRIN Indexes for Large Datasets
- GIN/GIST Indexes for Specialized Workloads
- Partial and Expression Indexes
- Partitioning: Scaling Large Tables
- Declarative Partitioning (Range, List, Hash)
- Partition Pruning
- Monitoring & Maintenance: Sustaining Performance
- pg_stat_statements: Identify Slow Queries
- Auto-Vacuum Tuning
- Kernel & OS Tuning: Removing System Bottlenecks
- Memory and Swap Settings
- I/O Scheduler and Disk Optimization
- Conclusion
- References
1. Memory Configuration: The Foundation of Performance
PostgreSQL relies heavily on memory to cache data, execute queries, and manage transactions. Misconfigured memory settings are the most common cause of performance bottlenecks. Here’s how to optimize key parameters:
shared_buffers: PostgreSQL’s Internal Cache
shared_buffers is the memory pool PostgreSQL uses to cache frequently accessed data and indexes. Unlike the OS cache (which caches files at the filesystem level), shared_buffers is PostgreSQL-aware, making it more efficient for database workloads.
-
Default: 128MB (way too small for modern servers).
-
Recommendation:
- For dedicated database servers: 25–50% of available RAM.
- For mixed-workload servers: 25% of RAM (leave the rest for the OS cache).
- Example: On a 16GB server, set
shared_buffers = 4GB; on a 64GB server,shared_buffers = 16GB.
-
Why: PostgreSQL works best when it can keep hot data in
shared_buffers, reducing expensive disk I/O.
work_mem: Memory for Sorting and Joins
work_mem defines the memory allocated per operation (e.g., sorts, hash joins, bitmap scans). If a query exceeds work_mem, PostgreSQL spills to disk (temp files), drastically slowing down execution.
-
Default: 4MB (conservative to avoid memory bloat).
-
Recommendation:
- Start with 16–64MB for OLTP workloads (many small queries).
- For data warehouses (large sorts/joins), increase to 128–512MB.
- Critical Note:
work_memis per operation, per connection. If 100 connections each run 2 sorts, total memory usage could be100 * 2 * work_mem. Avoid oversetting!
-
Tweak: Use session-level settings for large ad-hoc queries:
SET work_mem = '256MB'; -- Only for this session
maintenance_work_mem: Memory for Heavy Operations
maintenance_work_mem is reserved for maintenance tasks like VACUUM, CREATE INDEX, and ALTER TABLE. These operations are memory-intensive and benefit from extra resources.
- Default: 64MB.
- Recommendation: 1–4GB (up to 10% of RAM). For example, on a 64GB server, set
maintenance_work_mem = 4GB. - Why: Faster index creation and vacuuming reduce downtime for maintenance.
effective_cache_size: Hint for Query Planner
effective_cache_size tells the query planner how much memory is available for caching data (combining shared_buffers and OS cache). It doesn’t allocate memory but helps the planner choose better execution plans (e.g., using indexes vs. sequential scans).
- Recommendation: 50–75% of total RAM. For a 64GB server,
effective_cache_size = 48GB.
2. Connection Management: Balancing Throughput and Overhead
Each PostgreSQL connection consumes memory (via work_mem, shared_buffers, and process overhead). Too many connections waste resources; too few limit throughput.
max_connections: Limit Concurrent Connections
max_connections sets the maximum number of simultaneous connections.
- Default: 100.
- Problem: If your app opens 200 connections, PostgreSQL will reject new ones.
- Solution:
- For small apps: Increase to 200–300 (but monitor memory usage).
- For high-scale apps: Use connection pooling (e.g., PgBouncer) instead of raising
max_connections.
Connection Pooling with PgBouncer
Connection pooling reduces overhead by reusing existing connections instead of spawning new ones. PgBouncer is the de facto tool for this:
- Benefits: Lower memory usage, faster connection setup, and better control over
max_connections. - Setup: Install PgBouncer, configure
pgbouncer.inito point to your PostgreSQL instance, and setpool_mode = session(ortransactionfor read-heavy workloads).
3. Query Optimization: Tuning for Faster Execution
The PostgreSQL query planner relies on configuration hints and statistics to choose optimal execution plans. Fine-tuning these settings can drastically speed up slow queries.
random_page_cost and sequential_page_cost: I/O Cost Estimates
These parameters tell the planner how expensive random vs. sequential disk reads are.
- Default:
random_page_cost = 4.0(assumes slow HDDs).sequential_page_cost = 1.0.
- Tweak for SSDs: SSDs have near-identical random and sequential read speeds. Set:
This encourages the planner to use indexes for random access, which is faster on SSDs.random_page_cost = 1.1 sequential_page_cost = 1.0
jit: Just-In-Time Compilation
PostgreSQL 12+ supports JIT compilation for complex queries, accelerating execution of expressions (e.g., WHERE clauses with math or string functions).
- Enable for data warehouses:
jit = on jit_above_cost = 10000 -- Only JIT for queries costing >10k - Disable for OLTP: JIT adds overhead for small, fast queries.
4. WAL Tuning: Accelerating Writes and Crash Recovery
Write-Ahead Logging (WAL) ensures data integrity by writing changes to a log before applying them to the main database. Tuning WAL settings balances write performance and crash recovery speed.
wal_buffers: Buffer for WAL Data
wal_buffers caches WAL data in memory before writing to disk.
- Default: 16MB (or 1/32 of
shared_buffers, whichever is larger). - Recommendation: For write-heavy workloads, increase to 64–128MB:
wal_buffers = 64MB
checkpoint_completion_target: Smooth I/O
Checkpoints write dirty pages from shared_buffers to disk. By default, checkpoints are abrupt, causing I/O spikes. checkpoint_completion_target spreads checkpoints over time.
- Default: 0.9 (spread over 90% of the checkpoint interval).
- Recommendation: Keep at 0.9 for most workloads. For very write-heavy systems, lower to 0.5 to reduce recovery time.
wal_writer_delay: Frequency of WAL Writes
wal_writer_delay controls how often the WAL writer flushes cached WAL data to disk (default: 200ms). For write-heavy apps, reduce to 100ms to avoid large backlogs:
wal_writer_delay = 100ms
5. Indexing Strategies: Beyond Basic B-Trees
Indexes speed up read queries but slow down writes (inserts/updates/deletes). Use advanced index types to balance performance:
BRIN Indexes: For Large, Ordered Tables
Block Range Indexes (BRIN) are tiny indexes ideal for large tables with ordered data (e.g., timestamps, IDs). They store min/max values per data block, making them 10–100x smaller than B-trees.
- Use Case: Time-series data (e.g., sensor logs), large partitioned tables.
- Example:
CREATE INDEX idx_sensor_data_time ON sensor_data USING BRIN (timestamp);
GIN Indexes: For Full-Text Search and Arrays
Generalized Inverted Indexes (GIN) excel at indexing arrays, JSONB, and full-text search vectors.
- Use Case: Full-text search, JSONB fields with many keys.
- Example:
-- Full-text search index CREATE INDEX idx_articles_fts ON articles USING GIN (to_tsvector('english', content));
Partial and Expression Indexes
- Partial Indexes: Index only a subset of rows (e.g., active users).
CREATE INDEX idx_active_users ON users (email) WHERE is_active = true; - Expression Indexes: Index the result of a function (e.g., lowercase emails).
CREATE INDEX idx_users_lower_email ON users (lower(email));
6. Partitioning: Scaling Large Tables
Partitioning splits large tables into smaller “partitions,” reducing the data scanned per query. PostgreSQL supports declarative partitioning (range, list, hash) as of version 10.
Range Partitioning: Time-Series Data
Split tables by a range (e.g., dates). Queries filtering by the partition key will scan only relevant partitions.
- Example: Partition sales data by month:
-- Parent table CREATE TABLE sales ( id INT, sale_date DATE, amount NUMERIC ) PARTITION BY RANGE (sale_date); -- Monthly partitions CREATE TABLE sales_2023_01 PARTITION OF sales FOR VALUES FROM ('2023-01-01') TO ('2023-02-01'); CREATE TABLE sales_2023_02 PARTITION OF sales FOR VALUES FROM ('2023-02-01') TO ('2023-03-01');
Partition Pruning
Ensure the query planner skips irrelevant partitions by filtering on the partition key:
-- Scans only sales_2023_01
SELECT * FROM sales WHERE sale_date BETWEEN '2023-01-15' AND '2023-01-20';
7. Monitoring & Maintenance: Sustaining Performance
Even well-tuned systems degrade over time. Use these tools to stay ahead:
pg_stat_statements: Identify Slow Queries
The pg_stat_statements extension tracks execution statistics for all SQL statements.
- Enable:
Then restart PostgreSQL and run:shared_preload_libraries = 'pg_stat_statements' -- In postgresql.confCREATE EXTENSION pg_stat_statements; - Use: Find top 10 slowest queries by total time:
SELECT queryid, query, total_time, calls FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10;
Auto-Vacuum Tuning
PostgreSQL auto-vacuums to reclaim space from deleted/updated rows and update statistics. For tables with high write activity (e.g., 10k+ inserts/hour), adjust:
autovacuum_vacuum_scale_factor: Fraction of table size triggering vacuum (default 0.2 = 20%). Lower to 0.05 for large tables.autovacuum_analyze_scale_factor: Fraction triggeringANALYZE(default 0.1). Lower to 0.02 for frequent data changes.
Example for a high-write table:
ALTER TABLE high_write_table SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02
);
8. Kernel & OS Tuning: Removing System Bottlenecks
PostgreSQL performance depends on the underlying OS. Tune these kernel settings (e.g., in /etc/sysctl.conf):
Memory: Avoid Swapping
Set vm.swappiness = 1 (default 60) to prioritize RAM over swap, preventing slow disk-based memory access.
Shared Memory Limits
PostgreSQL uses System V shared memory. Increase limits:
kernel.shmmax = 68719476736 # 64GB (adjust for your RAM)
kernel.shmall = 16777216 # 64GB / 4KB page size
I/O Scheduler
For SSDs, use the deadline or none scheduler to reduce latency:
echo deadline > /sys/block/sda/queue/scheduler # Replace sda with your disk
Conclusion
PostgreSQL tuning is an iterative process. Start with memory and connection settings, then move to query optimization, indexing, and monitoring. Always test changes in staging, benchmark with tools like pgBench, and monitor performance post-deployment. By tailoring configurations to your workload, you’ll unlock PostgreSQL’s full potential for speed, scalability, and reliability.