Reading a Query Plan Well Enough to Fix It Yourself
Most query performance problems come down to the database choosing a sequential scan when an index scan would be dramatically faster, or an index existing but not being used because of how the query was written. Reading a query plan well enough to tell the difference is a skill that pays for itself the first time it saves you from adding an index that wouldn't have helped at all.
This walks through what to actually look for in EXPLAIN ANALYZE output, and the specific fixes for the patterns that show up most often.
Vendors Covered in this Article
Disclosure: We may earn a commission if you buy through some links on this page. It doesn't change what we recommend.
What EXPLAIN ANALYZE is actually telling you
Run EXPLAIN ANALYZE, not just EXPLAIN, since the former actually executes the query and reports real timing and row counts, while the latter only estimates. The two numbers worth comparing first are the planner's estimated row count against the actual row count at each step; a large gap between them means the planner's statistics are stale or the query's structure is confusing it, and that gap is often the real root cause even when the symptom looks like a missing index.
The four patterns that show up again and again
- Sequential scan on a large table with a selective filter: the clearest missing-index signal, especially when the filter column has high cardinality.
- Index exists but isn't used: often caused by a function wrapped around the indexed column in the WHERE clause, which prevents the planner from using a standard index at all.
- Nested loop join with a huge outer row count: usually means a join order the planner chose poorly, often fixable by adding a more selective index on the join or filter column.
- Bitmap heap scan spilling to a large recheck: suggests the index is only partially selective for this query and a composite index covering more of the filter conditions would help.
Most real-world slow queries are one of these four, not something exotic.
Why 'index everything' makes writes slower without fixing reads
Every index speeds up the specific read patterns it covers and slows down every write to that table, since the database has to maintain the index on every insert, update, and delete. Adding an index for a query that runs once a day while degrading write performance on a table that gets updated constantly is a bad trade even if it technically makes that one query faster. Check write volume on the table before adding an index, not just read frequency of the slow query.
Composite index column order actually matters
A composite index on columns (a, b) is efficient for a query filtering on a alone, or on a and b together, but does not help a query filtering on b alone; Postgres can't use a composite index starting from a column that isn't its leading column. Order composite index columns with the most selective, most commonly filtered-alone column first, and verify this against your actual query patterns rather than the order the columns happen to appear in the table schema.
Confirming the fix actually worked
After adding or changing an index, run EXPLAIN ANALYZE again and confirm the plan actually changed to use it, since the planner won't always switch immediately, particularly if table statistics are stale. Run ANALYZE on the table manually if needed, and compare the new actual execution time against the old one under a realistic data volume, not a small test table where every plan looks fast regardless of whether the index is actually helping.
A worked example: the index that didn't help until the query changed
A team adds an index on a status column they believe is the bottleneck in a slow report query, reruns EXPLAIN ANALYZE, and the planner still shows a sequential scan. Looking closer, the query wraps that column in a function call to normalize case before comparing it, which silently prevents the plain index from being used at all. The fix isn't a different index, it's either removing the function call from the query or building the index on the function's result directly, and only after that change does the plan finally switch to an index scan and the query's execution time drop.
What Good Looks Like
Solid query plan literacy means reading EXPLAIN ANALYZE's actual versus estimated row counts, recognizing the handful of recurring bad-plan patterns, and confirming any index change actually shifted the plan under realistic data volume.
Building The Capability (5-Stage Skill Ladder)
How to Get Started
Disclosure: We may earn a commission if you buy through some links on this page. It doesn't change what we recommend.
Frequently Asked Questions
Why would Postgres ignore an index that clearly exists on the filtered column?
The most common reason is a function or type cast wrapped around the column in the WHERE clause, which prevents a standard index from being used. Check whether the query compares the raw column directly, or wraps it in something like a date truncation or a lowercase conversion first.
Should we add an index for every slow query we find?
No. Weigh the read frequency of the slow query against the write frequency of the table, since every index adds write overhead. A query that runs rarely on a table updated constantly usually isn't worth indexing even if the query itself would technically get faster.
How do we know if our query plan problem is actually stale statistics rather than a missing index?
Compare the planner's estimated row count to the actual row count in EXPLAIN ANALYZE output. A large gap between the two, even where an appropriate index exists, points toward stale statistics; running ANALYZE on the table is often the actual fix in that case.
About the numbers
This guide doesn't quote a sourced benchmark. Figures in it are estimates or general guidance, so check them against your own numbers.
Related Guides
Reading a Query Plan Before You Add an Index
Adding an index without checking the query plan can leave the planner ignoring it entirely while every write pays the maintenance cost. A safer workflow.
Reading a Query Plan Well Enough to Know Which Index You Actually Need
How to read an EXPLAIN ANALYZE query plan to find a real missing-index problem, and why adding indexes to every slow query makes things worse, not better.
Why Is This Query Slow? A Query Plan Reading Guide
A Q&A guide to reading EXPLAIN ANALYZE output, choosing index types, and telling a genuinely missing index from a redundant one nobody's used.
Reading a Query Plan to Find the Index You're Actually Missing
A worked walkthrough of reading a Postgres query plan to find exactly which index is missing, instead of guessing which columns to index.
Reading a Postgres Query Plan Before You Add an Index
How to read an EXPLAIN ANALYZE output to find out whether a slow query actually needs an index, and the indexing mistakes that slow queries down.
Read the Query Plan Before You Add Another Index
How to use EXPLAIN ANALYZE to find real bottlenecks, why every index has a write cost, and when the fix is a query rewrite instead of an index.