Home/Engineer/Databases/PostgreSQL Index Optimizatio…
DatabasesAdvancedUbuntuDebianCentOSPostgreSQLSQL

PostgreSQL Index Optimization — Diagnosing Slow Queries and Index Strategy

A practical guide to finding slow queries with EXPLAIN ANALYZE and choosing the right index types to improve performance.

Contents
  1. Finding Slow Queries
  2. Reading EXPLAIN ANALYZE
  3. Creating Indexes
  4. Choosing an Index Type
  5. Checking Index Status
  6. VACUUM and Updating Statistics

Finding Slow Queries

SQL
-- Enable pg_stat_statements (postgresql.conf)
shared_preload_libraries = 'pg_stat_statements'

-- Install
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- Top 10 slowest queries
SELECT
  round(total_exec_time::numeric, 2) AS total_ms,
  calls,
  round(mean_exec_time::numeric, 2) AS avg_ms,
  left(query, 100) AS query_preview
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

Reading EXPLAIN ANALYZE

SQL
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders
WHERE user_id = 123 AND status = 'pending'
ORDER BY created_at DESC
LIMIT 10;

Key items:

ItemMeaning
Seq ScanFull table scan (no index)
Index ScanIndex used
Bitmap Heap ScanCombination of multiple indexes
actual time=0.1..5.2Actual execution time (ms)
Buffers: shared hit=8Cache hits (many misses indicate I/O issues)

Creating Indexes

SQL
-- Single column
CREATE INDEX idx_orders_user_id ON orders (user_id);

-- Composite index (highest selectivity first)
CREATE INDEX idx_orders_user_status ON orders (user_id, status);

-- Partial index (index only matching rows)
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE status = 'pending';

-- Expression index
CREATE INDEX idx_users_email_lower ON users (lower(email));

-- Online creation (without locking)
CREATE INDEX CONCURRENTLY idx_posts_category ON posts (category);

Choosing an Index Type

SQL
-- B-tree: default, range queries (=, <, >, BETWEEN, LIKE 'prefix%')
CREATE INDEX idx_b ON events (created_at);

-- Hash: equality only, fast
CREATE INDEX idx_h ON sessions USING HASH (session_token);

-- GIN: arrays, JSONB, full-text search
CREATE INDEX idx_gin ON posts USING GIN (tags);
CREATE INDEX idx_jsonb ON products USING GIN (metadata jsonb_path_ops);

Checking Index Status

SQL
-- Index list and usage counts for a table
SELECT
  indexname,
  pg_size_pretty(pg_relation_size(indexname::regclass)) AS size,
  idx_scan AS scans
FROM pg_stat_user_indexes
WHERE relname = 'orders'
ORDER BY idx_scan;

-- Unused indexes (consider dropping)
SELECT schemaname, relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND schemaname = 'public';

VACUUM and Updating Statistics

SQL
-- Update statistics (so the planner can choose a good plan)
ANALYZE orders;

-- VACUUM + ANALYZE
VACUUM ANALYZE orders;

-- Check autovacuum status
SELECT * FROM pg_stat_user_tables WHERE relname = 'orders';
#PostgreSQL#SQL
Editorial note

AI tools may assist drafting. Check the evidence in each guide for whether commands were run and which environment was used. Confirm the current docs for your version before applying instructions, and email us about errors. email us

Topic-related official docs (auto-linked · not per-claim sources)PostgreSQL documentation ↗

Questions & answers

Ask a question about this guide. We'll review and reply.