Data Engineering & Real-Time Event StreamsPlaybook3 min readUpdated September 2026

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:

  1. Add the candidate index in a non production environment first.
  2. Run EXPLAIN ANALYZE again on the target query.
  3. Confirm the plan now shows an index scan or index only scan against the new index specifically.
  4. If you built a composite index for filter and sort, check that the separate sort step disappeared from the plan.
  5. 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.

Executive Capability Standard

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)

1. Learn:Run EXPLAIN ANALYZE on your slowest known queries and read the plans to distinguish a genuinely missing index from a query that has to touch a large row count regardless.
2. Do Manually:Add and verify one composite index for your single worst offending query, confirming with EXPLAIN ANALYZE that Postgres actually uses it before moving to the next one.
3. Delegate:Assign a backend or database focused engineer to review indexing suggestions from any automated tool before they're applied.
4. Automate:Set up periodic EXPLAIN ANALYZE checks against your most frequent queries so index drift and stale statistics are caught before they become a customer facing slowdown.
5. Buy:Bring in a fractional CTO or database specialist if query performance issues are recurring and you don't have anyone with deep Postgres tuning experience in house.

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