Production RAG & Vector Data ArchitecturePlaybook3 min readUpdated September 2026

Reading a Query Plan to Find the Index You're Actually Missing

Teams often respond to a slow query by indexing whatever column looks like it should help, based on a guess about how the query works. Sometimes that guess is right. Often it isn't, and the new index sits unused while the actual slow query keeps scanning the whole table, because the real bottleneck was somewhere the guess didn't consider.

The query plan tells you exactly where the time is going, without guessing. Here's how to read one and turn it into the right index, using it as a worksheet rather than treating it as intimidating output to skim past. The same five steps work whether you're looking at a single slow endpoint or triaging a whole list of complaints from your monitoring dashboard.

Step 1: How do you get the actual plan, not the estimated one?

Running an explain command with the analyze option gives you the plan the database actually executed, including real row counts and real timing at each step, rather than the planner's estimate of what it expects to happen. The estimated plan alone can be misleading when the database's statistics are out of date, so always reach for the analyzed version when you're diagnosing a genuinely slow query rather than exploring hypothetically.

Step 2: find the step consuming the most actual time

A query plan is a tree of steps, and the slow one is rarely the whole query, it's usually one specific step within it. Read through the plan looking for the step with the largest gap between its own time and its children's combined time, since that gap is where that particular step's own work, not its inputs, is actually costing you.

Step 3: check whether that step is a sequential scan on a large table

A sequential scan reads every row in a table to find the ones matching your query's condition, which is fine for a small table and expensive for a large one. If the slow step is a sequential scan on a table with a meaningful row count, and the query filters on a specific column, that column is very likely your missing index, assuming the filter is selective enough that an index would actually narrow the search meaningfully rather than still touching most of the table.

Step 4: How do you check a column is selective enough to index?

An index on a column where most rows share the same value, like a boolean flag that's true for nearly every row, won't help much, because the database still has to check nearly every row either way, and it may reasonably choose to skip the index and scan the table anyway. Check how many distinct values the column actually has and how evenly rows are distributed across them before assuming an index is the fix, since a low-selectivity index is dead weight that still costs write performance without buying you the read performance you wanted.

Step 5: verify the plan actually changes after you add it

Adding an index and assuming it worked isn't verification, rerunning the same analyzed explain command and confirming the plan now uses an index scan instead of a sequential scan for that step is. Sometimes the planner still chooses not to use a new index, often because the table is small enough that a sequential scan is genuinely faster, or because the query's actual filter doesn't match what you assumed it was doing. Either way, the plan tells you the truth, not your assumption about what should have happened.

The whole process fits into five moves:

  1. Run explain with the analyze option to get the plan that actually executed, with real row counts and timings at each step.
  2. Find the step whose own time is largest compared with its children's combined time, since that gap is where the real work happens.
  3. Check whether that step is a sequential scan on a large table filtered by a specific column.
  4. Check the column's selectivity, meaning how many distinct values it has and how evenly rows are spread across them.
  5. Add the index, rerun the same analyzed explain, and confirm the plan now uses an index scan for that step.

A worked example: the index that didn't help

Say a team adds an index on a status column after a slow query complaint, but the query plan still shows a sequential scan afterward. Reading the plan more closely reveals the actual slow step was a join further down the tree, filtering on a different column entirely, one the team never looked at because the status column was the first thing that looked suspicious. The new index wasn't wrong to consider, it just wasn't the fix, and only reading the plan's actual bottleneck, rather than guessing from the query's surface, would have pointed at the join condition that mattered.

Executive Capability Standard

What Good Looks Like

A properly indexed database is tuned from reading real, analyzed query plans that show exactly where time is spent, not from guessing which columns look like they should be indexed based on the query's surface appearance.

Building The Capability (5-Stage Skill Ladder)

1. Learn:Learn to read an analyzed query plan for your specific database, including how to spot the step consuming the most actual time.
2. Do Manually:Manually run the analyzed explain command against your slowest known query and trace exactly where the time goes.
3. Delegate:Give one engineer ownership of query performance review so slow-query complaints get a consistent, plan-based diagnosis instead of ad hoc guessing.
4. Automate:Automate slow query logging so queries crossing a latency threshold get flagged for review without anyone needing to notice manually.
5. Buy:Use a managed database performance monitoring tool that surfaces missing-index recommendations based on real query patterns, rather than reviewing every plan by hand.

How to Get Started

Frequently Asked Questions

How do we know if a sequential scan is actually a problem or just fine for that table?

Check the table's row count and how often the query runs. A sequential scan on a small, rarely-queried table often costs less time overall than maintaining an index for it would, while the same scan on a large, frequently-hit table is a real performance problem worth fixing directly.

Can adding too many indexes ever hurt performance?

Yes. Every index adds overhead to every write against that table, since the database has to update each index alongside the actual row. Indexing a column that's rarely used in queries trades real write performance for read performance you're not actually benefiting from, so each index should be justified by an actual slow query, not added speculatively.

Should we index every column used in a WHERE clause?

Not automatically. Check selectivity first: a column with only a couple of distinct values across millions of rows rarely benefits from an index the way a column with many distinct values does. Reading the actual query plan after adding a candidate index is the only reliable way to confirm it's actually being used and actually helping.

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