Reading an EXPLAIN Plan to Find the Index You're Actually Missing
To find a missing index, read the EXPLAIN ANALYZE plan for the slow query and look for a sequential scan across a large table where an index would let Postgres jump straight to the relevant rows. Automated tools only suggest candidates, and shipping each one unread adds write overhead without speeding up any real query.
This walkthrough covers reading an EXPLAIN ANALYZE plan well enough to know when an index is genuinely the fix, and when the real problem is somewhere else entirely.
Reading a sequential scan for what it's actually telling you
A sequential scan on a large table isn't automatically wrong; Postgres's planner chooses one deliberately when it estimates that scanning the whole table is cheaper than using an available index, which is often correct for a query that's going to return a large fraction of the table's rows anyway. The problem case is a sequential scan against a large table for a query that should only be touching a small, targeted subset of rows, which usually means no useful index exists for that filter condition, not that the planner made a mistake.
Run EXPLAIN ANALYZE, not just EXPLAIN, since ANALYZE actually executes the query and reports real row counts and real timing, not just the planner's estimate. A big gap between the planner's estimated row count and the actual row count returned is itself a useful signal, often pointing at stale table statistics rather than a missing index.
Matching an index to the actual filter and sort, not just the filter
A single column index on the field in a WHERE clause is the obvious first fix, but it's often incomplete if the query also sorts or joins on additional columns. A composite index covering the filter column and the sort column together can let Postgres avoid a separate sort step entirely, which shows up in the plan as the sort operation disappearing rather than just the scan type changing.
Column order in a composite index matters: put the column used for equality filtering first and the column used for range filtering or sorting after it, since that order is what lets Postgres use the index efficiently for both parts of the query rather than falling back to a less efficient partial usage of the index.
Confirming the index actually gets used before shipping it
Adding an index and never confirming Postgres actually chooses to use it for the target query is a common way an automated suggestion turns into wasted write overhead with no read benefit. Run EXPLAIN ANALYZE again after adding the candidate index, in a non production environment first, and confirm the plan now shows an index scan or index only scan against the new index specifically, not just a plan that happens to look different.
Check whether the new index makes an existing index redundant rather than simply adding to the pile. A table with several overlapping indexes that all cover similar columns adds real write cost for every insert and update without providing proportional read benefit, which is exactly the failure mode automated indexing tools produce when suggestions are applied without this kind of review.
Verify a candidate index before shipping it:
- Add the candidate index in a non production environment first.
- Run EXPLAIN ANALYZE again on the target query.
- Confirm the plan now shows an index scan or index only scan against the new index specifically.
- If you built a composite index for filter and sort, check that the separate sort step disappeared from the plan.
- Drop the index if the planner ignores it, since an unused index only adds write overhead.
When the fix isn't an index at all
Some slow queries aren't index problems even after applying a good one; a query that fundamentally has to touch a large fraction of a huge table, an aggregate over a wide date range with no practical way to narrow it, is going to be slow regardless of indexing and needs a different approach, like a precomputed rollup table or a caching layer in front of the query. An EXPLAIN plan that still shows a large row count being processed even against the best available index is the signal that indexing alone has hit its limit for that particular query.
An automated tool that only ever suggests indexes will miss this category of problem entirely, since it's optimizing within the wrong solution space for that specific case. Reading the plan yourself, rather than trusting a tool's suggestion at face value, is what catches the difference between a genuinely missing index and a query that needs a structurally different approach.
What Good Looks Like
Good indexing practice means every candidate index is confirmed with EXPLAIN ANALYZE to actually get used by the target query, composite indexes match both the filter and sort columns, and queries that genuinely need a structural fix aren't forced into an indexing solution that can't help them.
Building The Capability (5-Stage Skill Ladder)
How to Get Started
Frequently Asked Questions
Is a sequential scan in an EXPLAIN plan always a sign something needs an index?
No. Postgres's planner chooses a sequential scan deliberately when it's cheaper than using an available index, which is often correct for a query returning a large fraction of the table. The problem case is specifically a sequential scan against a large table for a query that should only touch a small, targeted subset of rows.
Why would an automatically suggested index not actually improve our slow query?
Often because it doesn't cover both the filter and the sort or join columns the query actually uses, or because a query genuinely has to touch a large fraction of a huge table regardless of indexing. Always confirm with EXPLAIN ANALYZE that Postgres actually chooses to use a new index before assuming the suggestion fixed the problem.
Can adding too many indexes actually hurt our database?
Yes. Every index adds write overhead on every insert and update to that table, and a table with several overlapping indexes covering similar columns pays that cost repeatedly without proportional read benefit. Check whether a new index makes an existing one redundant before adding it, rather than accumulating indexes over time without review.
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.
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.
Where Latency Actually Hides in a Growing Data Pipeline
A walkthrough of where latency hides as a real-time pipeline grows, from producer batching to consumer lag, so you can find your own bottleneck fast.
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 Query Plan Well Enough to Fix It Yourself
A practical guide to reading EXPLAIN ANALYZE output, spotting the specific signs of a missing or unused index, and fixing the query plan that's actually slow.
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.