Reading a Postgres Query Plan Before You Add an Index
Adding an index is the reflexive fix for a slow query, and it's wrong often enough to be worth checking first. A query plan, not a guess, is what tells you whether an index will actually help, or whether the real problem is a query shape no index can fix.
EXPLAIN ANALYZE is the tool. Reading its output correctly is the skill most teams skip past on the way to just adding an index and hoping.
Reading EXPLAIN ANALYZE without getting lost in it
Run EXPLAIN ANALYZE on the actual slow query, not a simplified version of it, since query plans change based on the real data distribution and the real filter values. Look for a sequential scan on a large table where you expected an index scan, since that's the clearest sign a missing or unusable index is the problem.
Also check the gap between the planner's estimated row count and the actual row count at each step; a big gap usually means Postgres's statistics are stale, which can cause it to pick a bad plan even when the right index already exists.
When an index won't fix a slow query
A sequential scan on a small table is usually faster than an index scan would be, since the overhead of using an index exceeds just reading the whole table when the table's small enough to fit in memory. A query that filters on a low-selectivity column, like a boolean column where almost every row shares the same value, gets little benefit from an index on that column alone, since an index only helps when it meaningfully narrows the result set.
And a query that's slow because it's returning and processing an enormous result set has a query-shape problem, usually solved by pagination or a narrower filter, not an indexing problem at all.
Composite indexes: column order matters more than people expect
A composite index on status and created-date supports a query filtering on status alone, or on status and created-date together, but it doesn't help a query filtering on created-date alone, because a composite index is only usable from its leftmost column inward.
Put the column your queries filter on most consistently first, and the one that narrows the result set most, usually an equality filter before a range filter, ahead of a less selective one. Getting the order wrong is a common reason a team adds an index and sees no improvement, then concludes indexing didn't help when the real issue was column order.
The cost side nobody checks: writes and storage
Every index speeds up the reads it covers and slows down every write to that table, since the database has to maintain the index on every insert, update, and delete. An over-indexed table with many overlapping or rarely-used indexes pays that write cost on every mutation for indexes that queries barely use.
Periodically check for unused indexes, most databases expose usage statistics for this, and drop them; an index that never gets used in a query plan is pure write overhead and storage cost with no offsetting benefit.
Before you add an index, confirm each of these:
- EXPLAIN ANALYZE on the real slow query shows a sequential scan on a large table, not a small table that fits in memory.
- The filtered column is selective enough that an index skips most rows, unlike a boolean column where nearly every row shares the same value.
- Planner row estimates match actual row counts at each step, because a big gap points to stale statistics rather than a missing index.
- Composite indexes lead with the column your queries filter on most consistently, since they only work from the leftmost column inward.
- Existing indexes are actually used, because each unused one still slows every insert, update, and delete.
A worked example: diagnosing a slow dashboard query
Say a dashboard query filtering on an organization ID and a date range takes several seconds, and EXPLAIN ANALYZE shows a sequential scan on a multi-million-row table. Adding a composite index with the organization ID first, since every query filters on it and it's highly selective, turns that sequential scan into an index scan touching only the rows for one organization's date range.
If the plan still shows a sequential scan after adding the index, check whether the planner's row estimates are stale, since Postgres won't use an index it doesn't expect to help, even when it would.
Partial indexes for the common case where most rows don't matter
If a query only ever filters for rows in one particular state, active orders, unread notifications, pending approvals, and that state describes a small fraction of the table, a full index on the column wastes space and write overhead maintaining entries for rows no query will ever ask for. A partial index, built with a WHERE clause matching that same filter, indexes only the relevant rows, staying smaller and faster to maintain while still fully supporting the specific query it was built for.
This is an easy win teams miss because a standard index already works, just less efficiently than a partial one would, so there's no error prompting anyone to look closer.
Index maintenance after a large delete or update
A large delete or bulk update leaves index bloat behind, dead entries that still occupy space and slow down scans until routine maintenance cleans them up. If a table has recently gone through a large deletion and query performance on it hasn't improved the way you'd expect, check whether index maintenance has actually run, rather than assuming the index itself needs rebuilding from scratch. Most databases handle this automatically on a schedule, but a sudden large one-off deletion can outpace that schedule and benefit from a manual pass.
What Good Looks Like
A good indexing practice reads the actual query plan with EXPLAIN ANALYZE before adding an index, gets composite index column order right, and periodically drops indexes that query plans never actually use.
Building The Capability (5-Stage Skill Ladder)
How to Get Started
Frequently Asked Questions
Should we add an index every time a query feels slow?
No, check the query plan first with EXPLAIN ANALYZE. A slow query on a small table, or one filtering on a low-selectivity column, often won't benefit meaningfully from an index. Adding indexes without checking the plan tends to accumulate write overhead on indexes that rarely get used.
Why does our composite index not seem to be helping?
Check the column order. A composite index is only usable from its leftmost column inward, so an index on status and created-date won't help a query filtering on created-date alone. Put the column your queries filter on most consistently, and most selectively, first.
How many indexes are too many on one table?
There's no fixed number; the real question is whether each index is actually used in query plans. Check your database's index usage statistics periodically and drop anything that never shows up, since an unused index still costs write performance and storage with no offsetting read benefit.
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.
Continuous Device Verification for a Zero-Trust API
How continuous device and identity verification actually works in a zero-trust architecture, and where to draw the line for a small engineering team.
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.
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.