Model Context Protocol & Agentic ArchitecturePlaybook4 min readUpdated September 2026

Reading a Query Plan to Find a Missing Database Index

A query that ran fine for months suddenly takes seconds instead of milliseconds, and the table it queries hasn't obviously changed, no new columns, no schema migration anyone remembers. The usual explanation is that the table grew past a size where the query plan Postgres was using stopped being efficient, and the fix starts with reading the actual query plan rather than guessing at what index might help.

This walks through reading a query plan for a real slow query, finding the step that's actually expensive, and deciding whether an index is the right fix or whether the query itself needs to change.

The Symptom: A Query That Used to Be Fast

The classic pattern: a query filtering on a specific column performed fine when the table had a modest row count, because Postgres could scan the whole table faster than the overhead of using an index would have been worth. As the table grows, that same full scan gets proportionally slower, and at some point crosses from imperceptible to noticeably slow, often without any code change triggering it.

This is why the first response shouldn't be assuming the code broke something. Run an EXPLAIN ANALYZE on the actual slow query first, with real production-like data volume if you can, since a query plan on a small local development database often looks completely different from the same query's plan against your real table size.

How do you read a query plan from the bottom up?

A query plan reads as a tree, and the innermost, deepest nodes execute first, with results flowing up to the nodes above them. Read from the bottom, not the top, to find where time is actually being spent. Each node reports its own actual time separately from the cumulative time of everything below it, and the node where that difference is largest is usually where the real cost lives, not necessarily the outermost node with the largest total time.

Compare the planner's estimated row count against the actual row count at each node. A large mismatch between estimated and actual rows is one of the most common signals that the planner's statistics are stale or that the query is structured in a way the planner can't optimize well, both of which point toward different fixes than simply adding an index.

Sequential Scan Isn't Always the Problem

A sequential scan in the plan isn't automatically a bug. For a query that legitimately needs to touch most of a small or moderately sized table, a sequential scan can be faster than the overhead of using an index, and Postgres's planner is generally right about this tradeoff when its statistics are current. The problem case is a sequential scan on a large table for a query that's only supposed to touch a small, selective subset of rows.

Check selectivity before assuming an index will help: how small a fraction of the table does this specific filter actually match. An index helps most when a query is highly selective, touching a small slice of a large table. An index on a column with low selectivity, where most rows match the filter anyway, often doesn't get used by the planner at all, because a sequential scan is genuinely faster in that case.

How do you add an index, and what should you check first?

Before adding an index, confirm what it actually costs: every index slows down writes to that table, since each insert, update, or delete has to maintain the index alongside the table data, and it adds ongoing storage. An index that speeds up an infrequent report query but meaningfully slows down a high-frequency write path can be a net loss depending on your actual traffic mix.

Check whether an existing index already covers this case in a different order, or whether a composite index across the columns this specific query filters and sorts by would serve multiple slow queries at once rather than adding a narrow, single-purpose index for each one. After adding it, re-run the plan against the same query to confirm the planner is actually choosing to use the new index, since adding an index doesn't guarantee the planner will pick it.

Signs an Index Isn't Actually Being Used

After adding an index, watch for these signs that it isn't delivering the benefit you expected:

  • The query plan still shows a sequential scan after the index exists, which usually means the planner's statistics need updating or the query isn't written in a way the index can actually help with.
  • The index exists on the right column but in a data type or collation that doesn't match how the query filters, which silently prevents the planner from using it.
  • Write latency on the table increased noticeably after adding the index, without a corresponding read improvement large enough to justify the tradeoff.
  • The index was added for one specific query that changed or was removed later, and now exists purely as unused write overhead nobody's audited in a while.

Periodically review your indexes against actual query patterns, not just at the moment each one was added. An index that made sense a while back can become pure overhead as your application's real query patterns shift.

Executive Capability Standard

What Good Looks Like

Good indexing means every new index is added after reading an actual query plan, not guessed at, and existing indexes are periodically reviewed against real query patterns rather than left in place forever.

Building The Capability (5-Stage Skill Ladder)

1. Learn:Learn to read a query plan well enough to identify which specific node is actually expensive, not just which query is slow overall.
2. Do Manually:Run an explain analysis on your slowest handful of production queries manually and check whether each one's selectivity would actually benefit from an index.
3. Delegate:Have whoever owns the database layer review new indexes before they ship, checking write cost against read benefit for the actual traffic mix.
4. Automate:Set up periodic reporting on unused indexes and on queries still hitting sequential scans against large tables, so drift gets caught without a manual audit.
5. Buy:Bring in a database consultant for a deeper indexing review if slow queries keep recurring after the obvious fixes, since the remaining cases often need query restructuring, not just a new index.

How to Get Started

Frequently Asked Questions

Is a sequential scan always a sign we're missing an index?

No. For a query touching most of a small or moderately sized table, a sequential scan can genuinely be faster than using an index, and Postgres's planner usually gets this right when its statistics are current. Check the query's selectivity, how small a fraction of rows it actually matches, before assuming an index would help.

Does adding more indexes always make queries faster?

No, every index adds write overhead, since inserts, updates, and deletes all have to maintain it too. An index that speeds up an infrequent report but slows down a high-frequency write path can be a net loss. Weigh the read benefit against the write cost for your actual traffic mix before adding one.

How do we know if the planner is actually using an index we added?

Re-run the query plan on the same query after adding the index and confirm it now references the index instead of a sequential scan. Adding an index doesn't guarantee the planner chooses it, especially if the query is structured in a way that prevents it, or the statistics are stale.

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