A slow PostgreSQL query is the most common performance bottleneck in web applications — and the most fixable. This guide covers the systematic approach to identifying slow queries, reading EXPLAIN ANALYZE output, and applying the optimizations that have the highest ROI: indexing, query rewriting, and configuration tuning. From 2 seconds to 2 milliseconds.

The Optimization Workflow

  1. Identify: Enable pg_stat_statements, find the top 10 slowest/most frequent queries
  2. Analyze: Run EXPLAIN (ANALYZE, BUFFERS) on the slow query
  3. Diagnose: Is it a missing index? A bad query plan? Lock contention? Hardware?
  4. Fix: Apply the specific optimization, measure the improvement
  5. Prevent: Add an index, adjust work_mem, or rewrite the query permanently

Reading EXPLAIN ANALYZE Output

Node TypeWhat It MeansGood or Bad?Action
Seq ScanScanning every row in the tableBad for large tables (>10K rows)Add an index
Index ScanUsing index, reads index + heapGood for selective queries, bad for large result setsUsually fine; Index Only Scan is better
Index Only ScanIndex covers all needed columnsExcellent — no heap access neededKeep; consider covering indexes
Bitmap Index Scan → Bitmap Heap ScanCombines multiple indexes, then reads heapOK for AND/OR conditions on indexed columnsFine unless rows are very large
Nested LoopFor each row in A, look up BGood if A is small, B is indexedBad for large tables without index
Hash JoinBuild hash of one table, probe with otherGood for joining two large tablesUsually fine; ensure work_mem is sufficient
Merge JoinBoth inputs sorted, merge like zipperGood for pre-sorted inputs (from indexes)Excellent if both are index scans

Index Types and When to Use Them

Index TypeBest ForExampleSize Overhead
B-Tree (default)Equality, range, ORDER BY, <, >, BETWEENWHERE user_id = 123 / WHERE created_at > now() - interval '7 days'~40% of table size
Partial IndexQueries on a subset of rowsWHERE status = 'active' AND created_at > '2024-01-01' (index WHERE status = 'active')Small
Covering Index (INCLUDE)Index-Only Scans for specific columnsINDEX ON users (email) INCLUDE (name, avatar_url)Medium
GIN (Generalized Inverted)Full-text search, arrays, JSONBWHERE document @@ to_tsquery('search & query')Large
GiSTGeometric data, full-text searchWHERE location <@ ST_MakeEnvelope(...)Medium-Large
BRIN (Block Range)Very large tables, naturally sorted dataWHERE created_at BETWEEN ... (on 1B+ row tables)Very Small (<0.1%)

Common Optimizations by Root Cause

SymptomRoot CauseFixSpeedup
Seq Scan on large tableMissing indexCREATE INDEX ON table (filter_column)100-10,000x
Nested Loop with large inner tableMissing JOIN indexCREATE INDEX ON inner_table (join_key)50-500x
Sort using external merge (disk)work_mem too lowSET work_mem = '256MB' for this query5-20x
N+1 queries (ORM)Lazy loading in applicationUse eager loading (includes/joins)10-100x
Slow COUNT(*)MVCC visibility checksUse estimate: SELECT reltuples FROM pg_class WHERE relname = 'table'100-1000x
Table bloatDead tuples from UPDATE/DELETEVACUUM ANALYZE; adjust autovacuum settings2-10x

Key PostgreSQL Configuration Tuning

-- Check current settings
SHOW shared_buffers;        -- Default: 128MB. Set to 25% of RAM.
SHOW work_mem;              -- Default: 4MB. Increase to 64-256MB for reporting queries.
SHOW maintenance_work_mem;  -- Default: 64MB. Set to 10% of RAM for VACUUM/INDEX speed.
SHOW effective_cache_size;  -- Default: 4GB. Set to 75% of RAM (hint for planner).
SHOW random_page_cost;      -- Default: 4.0. Set to 1.1 for SSD (encourages index use).

-- Enable slow query logging
ALTER SYSTEM SET log_min_duration_statement = 1000;  -- Log queries > 1s
SELECT pg_reload_conf();

Bottom line: 90% of PostgreSQL performance problems are solved by adding the right index and adjusting work_mem. Before adding indexes, run EXPLAIN (ANALYZE, BUFFERS) on the slow query. If you see Seq Scan on a large table, add an index. If you see external merge on disk, increase work_mem. These two fixes alone resolve the vast majority of performance issues. See also: Full-Text Search Comparison and Database Migrations Guide.