Analyze and Optimize SQL Indexes with EXPLAIN
Identify missing, unused, and inefficient indexes by reading execution plans and measuring query cost with EXPLAIN.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Indexes are the primary tool for making SQL queries fast, but adding them blindly can waste space, slow writes, and even make queries slower. The right approach is to start with the execution plan. EXPLAIN and EXPLAIN ANALYZE reveal whether the database is scanning the whole table or using an index, and they estimate the cost of each step so you can target the biggest bottlenecks first.
When to Use
Use this resource when:
- A query is slower than expected and you suspect a missing index.
- You want to verify that a newly created index is being used.
- You are reviewing slow query logs or performance dashboards.
- You need to decide between a B-tree, GIN, or partial index.
Solution
Analyze a query with EXPLAIN ANALYZE
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
SELECT * FROM orders
WHERE customer_id = 1234
AND created_at > '2024-01-01'
ORDER BY created_at DESC
LIMIT 100;
-- Create a composite index if the plan shows a sequential scan
CREATE INDEX idx_orders_customer_created
ON orders (customer_id, created_at DESC);
Explanation
EXPLAIN (ANALYZE, BUFFERS) runs the query and reports actual execution time plus I/O statistics. Look for Seq Scan on large tables, which means the database is reading every row. If the filter is selective, a composite index on (customer_id, created_at) lets the database jump to the relevant rows and return them in sorted order. The index order should match the query’s equality columns first, then range columns, then sort columns.
Variants
| Index type | Best for | Example |
|---|---|---|
| B-tree | Equality and range | WHERE id = 5 or WHERE date > '2024-01-01' |
| GIN | Array, JSONB, full-text | WHERE tags @> ARRAY['x'] |
| BRIN | Very large, naturally ordered tables | Time-series data |
| Partial | Subset of rows | WHERE deleted_at IS NULL |
Best Practices
- Always measure before and after.
EXPLAIN ANALYZEgives concrete proof of improvement. - Index equality columns first. They are more selective than range columns.
- Keep indexes narrow. Include only columns the query actually needs.
- Drop unused indexes. They consume disk space and slow down writes.
- Monitor write performance. Each index adds cost to
INSERT,UPDATE, andDELETE.
Common Mistakes
- Adding an index for every slow query. Too many indexes hurt write throughput and maintenance.
- Wrong column order in composite indexes. The leading column must be the one used in equality filters.
- Indexing low-cardinality columns alone. An index on
statuswith only three values is rarely useful. - Forgetting to update statistics. Run
ANALYZEafter bulk loads so the planner has accurate row counts. - Assuming the planner will use the index. Always confirm with
EXPLAIN; hints are a last resort.
Frequently Asked Questions
Q: What is the difference between EXPLAIN and EXPLAIN ANALYZE? A: EXPLAIN shows the planned execution. EXPLAIN ANALYZE actually runs the query and reports real timing and rows processed.
Q: How do I know if an index is being used?
A: Look for Index Scan or Index Only Scan in the plan. Seq Scan on a large table usually means the index is not being used.
Q: Should I add an index to every foreign key column?
A: Usually yes, especially if the column is used in JOINs, WHERE clauses, or child lookups. But verify usage with EXPLAIN.
Related Resources
Read Replicas — Scale Reads Without Changing Application Logic
A practical guide to read replicas: setting up replication, routing read queries, handling replication lag, and scaling read-heavy workloads with PostgreSQL, MySQL, and cloud-managed replicas.
GuideSQL Performance Tuning — Indexes, Queries, and Explain Plans
A practical guide to optimizing SQL queries: indexing strategies, query rewriting, EXPLAIN plan analysis, and common anti-patterns to avoid.
RecipePostgreSQL Query Optimization and Indexing Strategies
Analyze and optimize slow PostgreSQL queries using EXPLAIN, proper indexing, partial indexes, and query rewriting to reduce execution time from seconds to milliseconds
RecipeFind and Remove Duplicate Rows in SQL
Detect duplicate records in SQL tables using GROUP BY and HAVING, then remove them safely while keeping the canonical row.
RecipeTraverse Hierarchical Data with Recursive CTEs
Query tree-like or graph-like structures in SQL using recursive common table expressions to walk parent-child relationships.