正在加载,请稍候…

PostgreSQL Performance Tuning: Indexes, Query Plans, and Configuration

Optimize PostgreSQL for production — EXPLAIN ANALYZE, index strategies (B-tree, GIN, BRIN), partial indexes, connection pooling with PgBouncer, and key configuration parameters.

PostgreSQL Performance Fundamentals

Slow queries are the #1 cause of application performance issues. Here's how to systematically diagnose and fix them.

EXPLAIN ANALYZE

-- Always use EXPLAIN (ANALYZE, BUFFERS) for real execution data
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > '2026-01-01'
GROUP BY u.id, u.name
ORDER BY order_count DESC
LIMIT 20;

-- Key metrics to look at:
-- Seq Scan vs Index Scan
-- Actual rows vs estimated rows (large discrepancy = stale stats)
-- Buffers: shared hit vs read (high read = disk I/O)
-- Loops: nested loop count

Index Types

-- B-tree (default): equality, range, ORDER BY
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_orders_created ON orders (created_at DESC);

-- Composite index: left-to-right prefix rule
CREATE INDEX idx_orders_user_status ON orders (user_id, status, created_at DESC);
-- Useful for: WHERE user_id = X AND status = 'pending'
-- Also: WHERE user_id = X ORDER BY created_at DESC

-- Partial index: index a subset of rows
CREATE INDEX idx_orders_pending ON orders (user_id, created_at)
WHERE status = 'pending';
-- Much smaller, faster for pending-only queries

-- GIN index: full-text search, JSONB, arrays
CREATE INDEX idx_posts_search ON posts USING gin(to_tsvector('english', title || ' ' || content));
CREATE INDEX idx_products_tags ON products USING gin(tags);  -- array column

-- BRIN: time-series data with physical correlation
CREATE INDEX idx_events_timestamp ON events USING brin(timestamp);
-- Very small, effective for append-only time-series

Query Optimization Patterns

-- Use CTEs for readability but be careful (CTE fence in PG < 12)
WITH user_stats AS MATERIALIZED (
  SELECT user_id, COUNT(*) as total, SUM(amount) as revenue
  FROM orders
  WHERE status = 'completed'
  GROUP BY user_id
)
SELECT u.name, s.total, s.revenue
FROM users u
JOIN user_stats s ON s.user_id = u.id
ORDER BY s.revenue DESC
LIMIT 10;

-- Avoid N+1: use window functions
SELECT
  id, name,
  COUNT(*) OVER (PARTITION BY department) AS dept_size,
  AVG(salary) OVER (PARTITION BY department) AS avg_dept_salary,
  salary - AVG(salary) OVER (PARTITION BY department) AS salary_diff
FROM employees;

-- Efficient pagination (cursor-based)
-- Bad: OFFSET 10000 scans 10000 rows
SELECT * FROM posts ORDER BY created_at DESC OFFSET 10000 LIMIT 20;

-- Good: cursor pagination
SELECT * FROM posts
WHERE created_at < '2026-05-01T12:00:00'
ORDER BY created_at DESC
LIMIT 20;

Key Configuration Parameters

# postgresql.conf tuning

# Memory
shared_buffers = 25% of RAM           # e.g., 4GB for 16GB server
effective_cache_size = 75% of RAM     # for query planner estimates
work_mem = 64MB                       # per sort/hash operation
maintenance_work_mem = 512MB          # for VACUUM, CREATE INDEX

# Checkpoints
checkpoint_completion_target = 0.9
wal_buffers = 64MB

# Parallelism
max_parallel_workers_per_gather = 4   # parallel query workers
max_parallel_workers = 8

# Logging
log_min_duration_statement = 1000     # log queries > 1 second
log_checkpoints = on
log_lock_waits = on

Connection Pooling with PgBouncer

# pgbouncer.ini
[databases]
myapp = host=127.0.0.1 port=5432 dbname=myapp

[pgbouncer]
pool_mode = transaction          # Best for most web apps
max_client_conn = 1000
default_pool_size = 25           # Actual PG connections
reserve_pool_size = 5
server_idle_timeout = 600

Autovacuum Tuning

-- Check table bloat
SELECT
  tablename,
  n_dead_tup,
  n_live_tup,
  round(n_dead_tup::numeric / NULLIF(n_live_tup, 0) * 100, 2) AS bloat_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;

-- Per-table autovacuum settings for high-write tables
ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor = 0.01,    -- vacuum at 1% dead tuples
  autovacuum_analyze_scale_factor = 0.01,
  autovacuum_vacuum_cost_delay = 2
);

Monitoring Queries

-- Find slow queries (requires pg_stat_statements)
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

-- Find missing indexes
SELECT relname, seq_scan, idx_scan,
  seq_scan - idx_scan AS diff
FROM pg_stat_user_tables
WHERE seq_scan > 100
ORDER BY diff DESC;

-- Find index usage
SELECT indexrelname, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0  -- unused indexes
AND NOT indisprimary
ORDER BY pg_relation_size(indexrelid) DESC;